Browser Agent Deep Dive
Comprehensive guide to browser automation modes, APIs, and integration patterns
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.
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.
| Layer | Module | Holds |
|---|---|---|
| Protocol (agents tier) | praisonaiagents.tools.protocols.browser | BrowserAction, BrowserActionType, BrowserObservation, BrowserSession |
| Implementation (Tier 2) | praisonai_browser | agent.py, cdp_agent.py, playwright_agent.py, server.py, sessions.py, _protocol_bridge.py |
| Wrapper (Tier 1) | praisonai.browser | Compatibility 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.
| Variable | Type | Default | Purpose |
|---|---|---|---|
PRAISONAI_BROWSER_SKIP_EXTENSION_WAIT | Truthy string (1, true, yes, on — case-insensitive) | Unset | Skip 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_REMOTE | true | Unset | Allow 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_ORIGINS | CSV of origins | Empty | Explicit 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
- Mock the websocket layer
Patch the websocket layer (
unittest.mock,pytest-asyncio, etc.) so the bridge server is never reached. - 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 — includingfalse/0— leaves the readiness check active. - Run against the mock
The readiness poll used to block on the aiohttp
/healthloop before the mock got a chance. With the skip set, the run hits the mock immediately.
Quick Start
- Extension mode (default)
bash praisonai browser run "Search for PraisonAI on Google" - CDP mode (headless)
bash # Start Chrome with remote debugging, then: praisonai browser run "Search for AI" --engine cdp --record-video - Playwright mode
bash praisonai browser run "Search for AI" --engine playwright --headless
Architecture Overview
Instruction Flow (Extension Mode)
This diagram shows exactly how your instruction flows through the system:
Key Points
- User → CLI: User provides goal via
praisonai browser run "goal" - CLI → Server: CLI connects over WebSocket, sends
start_session - Server → Extension: Server forwards
start_automationto Chrome extension - Extension → Page: Extension uses CDP to capture page state (DOM, screenshot)
- Extension → Server: Sends observation with page details
- Server → Agent: Passes observation to BrowserAgent
- Agent → LLM: Agent builds prompt with goal + context, gets action from LLM
- Server → Extension: Forwards action (click, type, scroll, etc.)
- Extension → Page: Executes action via CDP
- Loop: Repeats until agent returns
done: trueor 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
| Feature | Extension Mode | CDP Mode | Playwright Mode |
|---|---|---|---|
| Requires Extension | ✅ Yes | ❌ No | ❌ No |
| Headless Support | ❌ No | ✅ Yes | ✅ Yes |
| Multi-Browser | Chrome only | Chrome only | Chrome, Firefox, WebKit |
| Session Limit | 1 at a time | Unlimited | Unlimited |
| API Used | chrome.debugger | CDP WebSocket | Playwright API |
| Best For | Interactive use | Headless automation | Cross-browser testing |
| Extra Dependencies | None | aiohttp, websockets | playwright |
Extension Mode (Default)
How It Works
Backend APIs
| API | Location | Purpose |
|---|---|---|
chrome.debugger.attach() | Extension | Attach to tab for control |
chrome.debugger.sendCommand() | Extension | Execute CDP commands |
Runtime.evaluate | CDP | Execute JavaScript |
Input.dispatchMouseEvent | CDP | Simulate clicks |
Input.dispatchKeyEvent | CDP | Simulate typing |
Page.captureScreenshot | CDP | Take screenshots |
CLI Usage
# 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 \
--debugProgrammatic Usage
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 actionsLimitations
CDP Mode
How It Works
Backend APIs
| API | Purpose | Example |
|---|---|---|
GET /json | List all pages | http://localhost:9222/json |
DOM.getDocument | Get DOM tree | {"depth": 4} |
DOM.querySelector | Find element | {"selector": "#search"} |
Runtime.evaluate | Execute JS | {"expression": "..."} |
Input.dispatchMouseEvent | Click | {"type": "click", ...} |
Input.dispatchKeyEvent | Type | {"type": "keyDown", ...} |
Page.navigate | Go 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
# Start Chrome with remote debugging
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
--remote-debugging-port=9222 \
--user-data-dir=/tmp/chrome-debugCLI Usage
# 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 logsProgrammatic Usage
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
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
Backend APIs
| Playwright API | Purpose | CDP Equivalent |
|---|---|---|
page.goto(url) | Navigate | Page.navigate |
page.click(selector) | Click element | Input.dispatchMouseEvent |
page.fill(selector, text) | Type text | Input.dispatchKeyEvent |
page.screenshot() | Capture screenshot | Page.captureScreenshot |
page.content() | Get HTML | DOM.getDocument |
page.evaluate(fn) | Execute JS | Runtime.evaluate |
page.wait_for_selector() | Wait for element | Custom polling |
Prerequisites
# Install Playwright
pip install playwright
# Install browsers
playwright install chromium
playwright install firefox # Optional
playwright install webkit # OptionalCLI Usage
# Use Playwright mode
praisonai browser run "Search for AI" --engine playwright
# Headless mode
praisonai browser run "task" --engine playwright --headlessProgrammatic Usage
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
| Command | Description | Example |
|---|---|---|
praisonai browser pages | List all browser tabs | Shows 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 JavaScript | Returns result |
Python API
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:
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
Common Patterns
Sequential Tasks (CDP Mode)
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
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
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 linksTroubleshooting
Debug CLI Commands
# 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" --debugSession Tracking
Both agent-side and server-side sessions are tracked in the same SQLite database:
# 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_atWriting 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.
| Client | What 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
| Issue | Cause | Solution |
|---|---|---|
| “Another debugger attached” | Previous session not cleaned up | praisonai browser reload or restart Chrome |
| Session timeout (0 steps) | Extension not connected | Run praisonai browser doctor extension; if the bridge shows 0 connections, load the extension manually (see Manual Extension Load below) |
| Actions not executing | CDP commands failing | Enable --debug mode, check console logs |
| Back-to-back sessions fail | Extension mode limitation | Use CDP mode: --engine cdp |
| Side panel not loading | Invalid Chrome version | Check minimum_chrome_version in manifest.json |
Cryptic connection error on run | Bridge server not started | Follow 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
| Issue | Cause | Solution |
|---|---|---|
| Connection refused | Chrome not started with debug port | Start with --remote-debugging-port=9222 |
| Page not found | Invalid page ID | Run praisonai browser pages |
| Timeout | Page still loading | Use wait_for_element() |
| No pages returned | Chrome not running | Start Chrome with debug flag |
Playwright Mode Issues
| Issue | Cause | Solution |
|---|---|---|
| Browser not installed | pip install praisonai (and praisonai[browser]) no longer downloads browser binaries automatically — they are provisioned on demand | playwright install chromium (add --with-deps in Docker / CI) |
| Selector not found | Wrong selector or slow page | Add delays or use wait_for_selector() |
Chrome Extension Console Logs
- Go to
chrome://extensions - Find “PraisonAI Browser Agent”
- Click “Service worker” link to open DevTools
- 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.
# Try the automated launch first — it may still work depending on Chrome build
praisonai browser launchWhen auto-load fails, launch prints the exact Load unpacked path plus a curl verification step instead of a generic error. Follow the recipe below.
- Open your daily Chrome
Use your normal Chrome (Work profile) — not a fresh debug profile.
chrome://extensions - Enable Developer mode
Toggle Developer mode on (top-right).
- Load the dist folder
Click Load unpacked and select the dist path printed by
praisonai browser launch(typically~/.praisonai/browser/extension/dist). - 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 startCheck
/healthdirectly (expectextension_connections >= 1):bash curl http://127.0.0.1:8765/healthA connected extension returns:
json { "status": "ok", "connections": 1, "extension_connections": 1, "sessions": 0, "extension_busy": false, "active_session_id": null }extension_busyandactive_session_id(added in PraisonAI #3095 hardening) show whether a session currently owns the extension — a healthy-and-idle bridge reportsextension_busy: false. Servers older than that commit omit both fields; treat missing asfalse/null. - 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.comAction 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.
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.