Browser Agent Deep Dive

Comprehensive guide to browser automation modes, APIs, and integration patterns

Diagram

Browser Agent Deep Dive

📋 User Request

⚙️ Browser Agent Deep Dive

✅ Result

python
from praisonaiagents import Agent

agent = Agent(name="browser-agent", instructions="Browse the web and complete tasks in Chrome.")
agent.start("Search for PraisonAI documentation and summarise the install steps.")

The user describes a web goal; the browser agent drives Chrome via extension, CDP, or Playwright.

Diagram

Browser Agent

📝 Web Goal

⚙️ Engine: Extension / CDP / Playwright

🤖 Browser Agent

✅ Task Result

Browser Agent Deep Dive

This guide explains how PraisonAI browser automation works under the hood, covering all execution modes, APIs, and integration patterns.

Package Layout

Heavy browser code lives in the standalone Tier-2 package praisonai_browser; the agents tier keeps only lightweight protocol types.

LayerModuleHolds
Protocol (agents tier)praisonaiagents.tools.protocols.browserBrowserAction, BrowserActionType, BrowserObservation, BrowserSession
Implementation (Tier 2)praisonai_browseragent.py, cdp_agent.py, playwright_agent.py, server.py, sessions.py, _protocol_bridge.py
Wrapper (Tier 1)praisonai.browserCompatibility shim re-exporting praisonai_browser

Install with pip install praisonai-browser. Import from praisonai_browser; from praisonai.browser import … still works via the shim.

Environment Variables

Environment variables tune the bridge server’s security posture and extension-readiness behaviour before a run starts.

Diagram

Extension Readiness

env var truthy

no

▶️ run_browser_agent

Skip wait?

⚡ straight to websocket

⏱ poll /health up to min 15s, timeout

📤 start_session

VariableTypeDefaultPurpose
PRAISONAI_BROWSER_SKIP_EXTENSION_WAITTruthy string (1, true, yes, on — case-insensitive)UnsetSkip the pre-connect /health readiness poll in run_browser_agent_with_progress. For unit tests / mocked bridges where no live Chrome extension is present.
PRAISONAI_BROWSER_ALLOW_REMOTEtrueUnsetAllow BrowserServer to bind a non-loopback host. Without it, any host other than 127.0.0.1 / localhost / ::1 is forced back to 127.0.0.1 with a SECURITY warning.
BROWSER_CORS_ORIGINSCSV of originsEmptyExplicit allow-list for CORS + WebSocket Origin validation. Chrome extensions still pass via the chrome-extension:// regex, regardless of this list.

The skip only triggers on the truthy values above. PRAISONAI_BROWSER_SKIP_EXTENSION_WAIT=false (or 0, no, off, or any inherited value) leaves the readiness check active.

Testing without a live extension

  1. Mock the websocket layer

    Patch the websocket layer (unittest.mock, pytest-asyncio, etc.) so the bridge server is never reached.

  2. Enable the skip
    python
    import os
    
    os.environ["PRAISONAI_BROWSER_SKIP_EXTENSION_WAIT"] = "1"

    Any of 1, true, yes, on (case-insensitive) enables the skip. Anything else — including false / 0 — leaves the readiness check active.

  3. Run against the mock

    The readiness poll used to block on the aiohttp /health loop before the mock got a chance. With the skip set, the run hits the mock immediately.

Quick Start

  1. Extension mode (default)
    bash
    praisonai browser run "Search for PraisonAI on Google"
  2. CDP mode (headless)
    bash
    # Start Chrome with remote debugging, then:
    praisonai browser run "Search for AI" --engine cdp --record-video
  3. Playwright mode
    bash
    praisonai browser run "Search for AI" --engine playwright --headless

Architecture Overview

Diagram

Browser

Backend

Modes

User

CLI Command

Python Code

Extension Mode

CDP Mode

Playwright Mode

Bridge Server
FastAPI + WebSocket

BrowserAgent
praisonaiagents

LLM API
GPT/Gemini

Chrome Extension
CDP Client

Chrome
Remote Debug

Chrome/Firefox/WebKit


Instruction Flow (Extension Mode)

This diagram shows exactly how your instruction flows through the system:

Diagram
Web PageCDP ClientChrome ExtensionLLM APIBrowserAgent(praisonaiagents)Bridge Server(FastAPI)CLIUserWeb PageCDP ClientChrome ExtensionLLM APIBrowserAgent(praisonaiagents)Bridge Server(FastAPI)CLIUserParse args, connect to serverBuild observation:url, title, elements, screenshotTrack action in historypraisonai browser run “Search for AI”WebSocket: start_session(goal)Create session_idBrowserAgent(model, session_id)WebSocket: start_automation(goal)chrome.debugger.attach(tabId)DOM.getDocumentDOM treePage.captureScreenshotScreenshotobservation(page_state)process_observation()Build prompt with goal + page stateChat completion requestJSON action response{“action”: “type”, “selector”: “action(type, selector, text)Runtime.evaluate (find element)Click/Type/ScrollAction resultSuccess/Errorobservation(new page_state)process_observation()“Is goal achieved?”{“action”: “done”, “done”: true}Done!status: completed✅ Task completed!

Key Points

  1. User → CLI: User provides goal via praisonai browser run "goal"
  2. CLI → Server: CLI connects over WebSocket, sends start_session
  3. Server → Extension: Server forwards start_automation to Chrome extension
  4. Extension → Page: Extension uses CDP to capture page state (DOM, screenshot)
  5. Extension → Server: Sends observation with page details
  6. Server → Agent: Passes observation to BrowserAgent
  7. Agent → LLM: Agent builds prompt with goal + context, gets action from LLM
  8. Server → Extension: Forwards action (click, type, scroll, etc.)
  9. Extension → Page: Executes action via CDP
  10. Loop: Repeats until agent returns done: true or timeout

Execution Modes

Engine × Option Compatibility

praisonai browser run accepts the same 14 options on every engine, but most only reach the wire on --engine cdp. On the default extension engine only goal, model, and max-steps are forwarded; playwright and hybrid additionally honour url but not the CDP-only extras (--vision, --screenshots, --record-video, --max-retries, --no-record). As of PR #4313 the CLI warns instead of silently discarding an engine-only flag.

See the full Engine × Option matrix in the CLI reference for the authoritative per-flag breakdown.

Comparison Table

FeatureExtension ModeCDP ModePlaywright Mode
Requires Extension✅ Yes❌ No❌ No
Headless Support❌ No✅ Yes✅ Yes
Multi-BrowserChrome onlyChrome onlyChrome, Firefox, WebKit
Session Limit1 at a timeUnlimitedUnlimited
API Usedchrome.debuggerCDP WebSocketPlaywright API
Best ForInteractive useHeadless automationCross-browser testing
Extra DependenciesNoneaiohttp, websocketsplaywright

Extension Mode (Default)

How It Works

Diagram
LLMCDP ClientChrome ExtensionBridge ServerCLILLMCDP ClientChrome ExtensionBridge ServerCLIloop[Until done or timeout]WebSocket connectstart_session(goal)start_automationchrome.debugger.attachCapture page stateobservation(elements, url, screenshot)Build prompt + get actionaction(click/type/scroll)Execute actionchrome.debugger.detachDisplay result

Backend APIs

APILocationPurpose
chrome.debugger.attach()ExtensionAttach to tab for control
chrome.debugger.sendCommand()ExtensionExecute CDP commands
Runtime.evaluateCDPExecute JavaScript
Input.dispatchMouseEventCDPSimulate clicks
Input.dispatchKeyEventCDPSimulate typing
Page.captureScreenshotCDPTake screenshots

CLI Usage

bash
# Basic usage (extension mode is default)
praisonai browser run "Search for PraisonAI on Google"

# With options
praisonai browser run "task" \
    --url https://google.com \
    --model gpt-4o \
    --timeout 120 \
    --debug

Programmatic Usage

python
from praisonai_browser import BrowserServer, BrowserAgent

# Start server
server = BrowserServer(port=8765, model="gpt-4o-mini")
server.start_background()  # Runs in thread

# Create agent (requires extension to be connected)
agent = BrowserAgent(model="gpt-4o-mini", session_id="my-session")

# Agent is called via WebSocket, not directly
# Extension sends observations → Server calls agent → Extension executes actions

Limitations


CDP Mode

How It Works

Diagram
LLMChromeWebSocketCDP AgentCLILLMChromeWebSocketCDP AgentCLIloop[Until done or timeout]run(goal)Connect to chrome://localhost:9222DOM.getDocumentPage.captureScreenshotGet next actionRuntime.evaluate / Input.dispatch*Close connectionDisplay result

Backend APIs

APIPurposeExample
GET /jsonList all pageshttp://localhost:9222/json
DOM.getDocumentGet DOM tree{"depth": 4}
DOM.querySelectorFind element{"selector": "#search"}
Runtime.evaluateExecute JS{"expression": "..."}
Input.dispatchMouseEventClick{"type": "click", ...}
Input.dispatchKeyEventType{"type": "keyDown", ...}
Page.navigateGo to URL{"url": "..."}

Retry & alternative-selector strategies

When _execute_action reports success=false, error="selector matched no element: …", CDPBrowserAgent.run() retries with three alternative selectors before propagating the failure to the LLM. See Browser CLI → Retry & selector fallback for the full flow diagram.

Fixed in PR #4312: before the fix, missing selectors returned success=true, so the retry loop was unreachable for the one failure it was written to handle.

Prerequisites

bash
# Start Chrome with remote debugging
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
    --remote-debugging-port=9222 \
    --user-data-dir=/tmp/chrome-debug

CLI Usage

bash
# Use CDP mode explicitly
praisonai browser run "Search for AI" --engine cdp

# CDP-specific commands (no extension needed)
praisonai browser pages              # List all tabs
praisonai browser dom <PAGE_ID>      # Get DOM tree
praisonai browser content <PAGE_ID>  # Read page text
praisonai browser js <PAGE_ID> "document.title"  # Execute JS
praisonai browser console <PAGE_ID>  # Capture logs

Programmatic Usage

python
from praisonai_browser import CDPBrowserAgent
from praisonai_browser.cdp_utils import get_pages, execute_js, get_dom

# Direct CDP control
async def example():
    # List pages
    pages = await get_pages(port=9222)
    for page in pages:
        print(f"{page.id}: {page.title}")

    # Execute JavaScript
    result = await execute_js(pages[0].id, "document.title")
    print(f"Title: {result}")

    # Get DOM
    dom = await get_dom(pages[0].id, depth=3)

    # Read page content
    from praisonai_browser.cdp_utils import read_page
    content = await read_page(pages[0].id)

# Run full automation
async def automate():
    agent = CDPBrowserAgent(
        port=9222,
        model="gpt-4o-mini",
        headless=False,
    )
    result = await agent.run("Search for PraisonAI on Google")
    print(result)

Sync Wrappers

python
from praisonai_browser.cdp_utils import (
    get_pages_sync,
    execute_js_sync,
    get_dom_sync,
    read_page_sync,
    get_console_sync,
)

# Synchronous usage
pages = get_pages_sync(port=9222)
title = execute_js_sync(pages[0].id, "document.title")

Playwright Mode

How It Works

Diagram
LLMBrowserPlaywrightPlaywright AgentCLILLMBrowserPlaywrightPlaywright AgentCLIloop[Until done or timeout]run(goal)launch(browser_type)Start Chrome/Firefox/WebKitpage.content(), screenshot()Get next actionpage.click() / fill() / etcbrowser.close()Display result

Backend APIs

Playwright APIPurposeCDP Equivalent
page.goto(url)NavigatePage.navigate
page.click(selector)Click elementInput.dispatchMouseEvent
page.fill(selector, text)Type textInput.dispatchKeyEvent
page.screenshot()Capture screenshotPage.captureScreenshot
page.content()Get HTMLDOM.getDocument
page.evaluate(fn)Execute JSRuntime.evaluate
page.wait_for_selector()Wait for elementCustom polling

Prerequisites

bash
# Install Playwright
pip install playwright

# Install browsers
playwright install chromium
playwright install firefox  # Optional
playwright install webkit   # Optional

CLI Usage

bash
# Use Playwright mode
praisonai browser run "Search for AI" --engine playwright

# Headless mode
praisonai browser run "task" --engine playwright --headless

Programmatic Usage

python
from praisonai_browser.playwright_agent import PlaywrightBrowserAgent

async def example():
    agent = PlaywrightBrowserAgent(
        model="gpt-4o-mini",
        browser_type="chromium",  # or "firefox", "webkit"
        headless=True,
    )

    result = await agent.run(
        goal="Search for PraisonAI on Google",
        start_url="https://google.com",
    )
    print(result)

Page Inspection Commands

These commands work via CDP (Chrome must be running with --remote-debugging-port=9222).

CLI Reference

CommandDescriptionExample
praisonai browser pagesList all browser tabsShows ID, title, URL
praisonai browser dom <id>Get DOM tree--depth 4
praisonai browser content <id>Read page as text--limit 2000
praisonai browser console <id>Capture console logs--timeout 2
praisonai browser js <id> "code"Execute JavaScriptReturns result

Python API

python
from praisonai_browser.cdp_utils import (
    get_pages,      # async: List[PageInfo]
    get_dom,        # async: Dict (DOM tree)
    read_page,      # async: str (page text)
    get_console,    # async: List[Dict] (log entries)
    execute_js,     # async: Any (JS result)
    wait_for_element,  # async: bool
)

# Synchronous versions also available:
# get_pages_sync, get_dom_sync, read_page_sync, etc.

Agent Memory & Sessions

Session Isolation

Each browser session now uses Agent’s built-in session management:

python
from praisonai_browser import BrowserAgent

# Session is auto-generated or can be provided
agent = BrowserAgent(
    model="gpt-4o-mini",
    session_id="my-unique-session",  # For memory isolation
)

# Reset for new session (clears chat_history)
agent.reset(new_session_id="new-session-id")

Memory Flow

Diagram

Agent

Session 1

Observation

Action

Observation

Action

chat_history
accumulates/reset


Common Patterns

Sequential Tasks (CDP Mode)

python
import asyncio
from praisonai_browser import CDPBrowserAgent

async def run_multiple_tasks():
    agent = CDPBrowserAgent(port=9222, model="gpt-4o-mini")

    tasks = [
        "Search for AI on Google",
        "Go to GitHub and search for praisonai",
        "Navigate to praison.ai/docs",
    ]

    for task in tasks:
        result = await agent.run(task)
        print(f"✅ {task}: {result['status']}")
        await asyncio.sleep(2)  # Pause between tasks

asyncio.run(run_multiple_tasks())

Headless Screenshot

python
from praisonai_browser.playwright_agent import PlaywrightBrowserAgent

async def take_screenshot():
    agent = PlaywrightBrowserAgent(
        headless=True,
        browser_type="chromium",
    )

    result = await agent.run(
        "Go to praison.ai/docs and take a screenshot",
        start_url="https://praison.ai/docs",
    )

Page Scraping

python
from praisonai_browser.cdp_utils import get_pages, read_page, execute_js

async def scrape_page():
    pages = await get_pages()

    # Find the right page
    target = next(p for p in pages if "google" in p.url.lower())

    # Get text content
    content = await read_page(target.id)

    # Or execute custom JS
    links = await execute_js(
        target.id,
        """
        Array.from(document.querySelectorAll('a'))
            .map(a => ({href: a.href, text: a.textContent.trim()}))
            .slice(0, 10)
        """
    )
    return links

Troubleshooting

Debug CLI Commands

bash
# Check server health
praisonai browser doctor

# List all sessions with step counts
praisonai browser sessions --limit 10

# View session history (step-by-step)
praisonai browser history <session_id>

# Reload extension after code changes
praisonai browser reload

# Run with debug mode
praisonai browser run "goal" --debug

Session Tracking

Both agent-side and server-side sessions are tracked in the same SQLite database:

bash
# Database location
~/.praisonai/browser_sessions.db

# Tables:
#   sessions: session_id, goal, status, started_at, current_url
#   steps: session_id, step_number, observation, action, thought, created_at

Writing Your Own Bridge Client

Only sockets that connect to ws://127.0.0.1:8765/ws without an Origin header can claim a role with register.

ClientWhat to do
Chrome offscreen document (no Origin)Send {"type": "register", "client": "extension"} on open
Python CLI (no Origin)Nothing — unregistered sockets default to CLI. Optionally send {"type": "register", "client": "cli"}
Browser tab (web Origin)Cannot register — rejected with INVALID_ROLE

Re-registering as cli is blocked once the socket has a session in flight. Details: Role Registration.

Extension Mode Issues

IssueCauseSolution
“Another debugger attached”Previous session not cleaned uppraisonai browser reload or restart Chrome
Session timeout (0 steps)Extension not connectedRun praisonai browser doctor extension; if the bridge shows 0 connections, load the extension manually (see Manual Extension Load below)
Actions not executingCDP commands failingEnable --debug mode, check console logs
Back-to-back sessions failExtension mode limitationUse CDP mode: --engine cdp
Side panel not loadingInvalid Chrome versionCheck minimum_chrome_version in manifest.json
Cryptic connection error on runBridge server not startedFollow the two-terminal setup in the friendly message: start it with praisonai browser start --port 8765, then rerun. See Browser Agent → If the bridge server is not running.

CDP Mode Issues

IssueCauseSolution
Connection refusedChrome not started with debug portStart with --remote-debugging-port=9222
Page not foundInvalid page IDRun praisonai browser pages
TimeoutPage still loadingUse wait_for_element()
No pages returnedChrome not runningStart Chrome with debug flag

Playwright Mode Issues

IssueCauseSolution
Browser not installedpip install praisonai (and praisonai[browser]) no longer downloads browser binaries automatically — they are provisioned on demandplaywright install chromium (add --with-deps in Docker / CI)
Selector not foundWrong selector or slow pageAdd delays or use wait_for_selector()

Chrome Extension Console Logs

  1. Go to chrome://extensions
  2. Find “PraisonAI Browser Agent”
  3. Click “Service worker” link to open DevTools
  4. Check Console for [PraisonAI] and [Bridge] logs

Manual Extension Load (Chrome 137+ / Windows)

Chrome 137+ on Windows blocks the automated --load-extension flag praisonai browser launch uses, so the debug profile opens empty. Load the extension into your daily Chrome once and it just works.

bash
# Try the automated launch first — it may still work depending on Chrome build
praisonai browser launch

When auto-load fails, launch prints the exact Load unpacked path plus a curl verification step instead of a generic error. Follow the recipe below.

Diagram
/healthExtensionChrome 137+ (Windows)praisonai browser launchUser/healthExtensionChrome 137+ (Windows)praisonai browser launchUserpraisonai browser launch “goal”–load-extension=<dist>(blocked silently — MV3 policy)poll extension_connections0Manual setup: chrome://extensions → Load unpacked → <dist>Load unpacked in daily Work ChromeWebSocket connect (Origin: chrome-extension://…)praisonai browser doctor extensionGET /health{extension_connections: 1}✅ Extension connected to bridge
  1. Open your daily Chrome

    Use your normal Chrome (Work profile) — not a fresh debug profile.

    chrome://extensions
  2. Enable Developer mode

    Toggle Developer mode on (top-right).

  3. Load the dist folder

    Click Load unpacked and select the dist path printed by praisonai browser launch (typically ~/.praisonai/browser/extension/dist).

  4. Confirm the bridge connection

    Open the PraisonAI side panel and confirm it connects to ws://127.0.0.1:8765/ws.

    If you launched with --no-server, start the bridge first:

    bash
    praisonai browser start

    Check /health directly (expect extension_connections >= 1):

    bash
    curl http://127.0.0.1:8765/health

    A connected extension returns:

    json
    {
      "status": "ok",
      "connections": 1,
      "extension_connections": 1,
      "sessions": 0,
      "extension_busy": false,
      "active_session_id": null
    }

    extension_busy and active_session_id (added in PraisonAI #3095 hardening) show whether a session currently owns the extension — a healthy-and-idle bridge reports extension_busy: false. Servers older than that commit omit both fields; treat missing as false / null.

  5. Verify
    bash
    praisonai browser doctor extension
    # expect: ✅ Extension connected to bridge (1 connection(s))

Common Log Patterns

# Successful click:
[Bridge] Clicking element: #submit

# Failed click:
[Bridge] Click failed: All click methods failed for: #submit

# Session cleanup:
[PraisonAI] Cleaning up previous session (tab 12345)...

# Observation sent:
[PraisonAI] Step 0: https://google.com

Action Verification

All actions return { success, error }:

  • success=true: CDP operation completed
  • success=false: Error message indicates what failed

The error is sent in the next observation, allowing the LLM to retry or try alternative actions.

Selector misses on click / type / clear_input show as error: "selector matched no element: <selector>" and trigger the alternative-selector strategies before reaching the LLM.

Best Practices

Pick the engine for your use case

Use extension mode for interactive debugging, CDP for headless Chrome automation, and Playwright when you need Firefox or WebKit or reliable multi-session runs.

Diagram

Interactive, on your own Chrome

Headless / server / Chrome 137+ Windows locked down

Cross-browser, multi-session, or CI

Need to drive a browser?

Extension mode
manual Load unpacked once

CDP mode
–engine cdp –remote-debugging-port=9222

Playwright mode
–engine playwright

Isolate sessions with session_id

Pass a unique session_id per task so chat_history does not leak between parallel or sequential automations. Call agent.reset() between unrelated goals.

Start Chrome with remote debugging for CDP

CDP mode requires Chrome on --remote-debugging-port=9222. Run praisonai browser pages to confirm tabs before starting an agent run.

Diagnose failures with doctor and –debug

Run praisonai browser doctor when extension sessions stall, and add --debug to surface CDP and bridge logs. Check ~/.praisonai/browser_sessions.db for step history.

praisonai browser doctor extension decides success by reading the bridge /health extension_connections count (source of truth — works even when the extension is loaded into your daily Chrome instead of the debug profile). CDP :9222 is reported as informational. A doctor failure now means “the extension is genuinely not connected to the bridge” — follow the manual-load steps above.


Related