Catalog PyPI
PACKAGE EXPLORERView in registry

agent-cdp

PyPI

Scoped Event System for agent-era browser automation

ai-agentbrowser-automationbrowser-usecdpevent-systemscoped-eventswatchdog
Versions collected1
Direct dependencies6From release 0.5.0
Last observed20 Oct, 21:37 UTC

README

Version 0.5.0 documentation

agent-cdp

Scoped Event System for agent-era browser automation.

A Qt-inspired, connection-based event framework designed for AI agent browser automation pipelines. Replaces the single-bus model (bubus/browser-use) with scoped, concurrent, priority-aware event dispatch — enabling multi-tab isolation, zero-latency security gating, and N:M connection topologies.

Version 0.5.0 implements explicit delivery contracts, independent waiting and execution budgets, cancellation, bounded queues, prefix draining, and resource ownership. See the migration guide, 24-requirement acceptance map, and changelog.

A scope is a serial event scheduling domain. BrowserContext permissions, target ownership, and security isolation across runs remain the caller's responsibility.

Who is this for?

  • Browser automation framework authors building AI agent pipelines (like browser-use, Skyvern, Agent-TARS) who need per-tab event isolation and concurrent dispatch
  • Multi-agent system developers coordinating multiple AI agents operating on the same browser instance across different tabs
  • Watchdog/plugin authors who need zero-latency Direct dispatch for security checks, popup dismissal, and crash recovery — without bypassing the event system
  • Anyone outgrowing bubus or ad-hoc Playwright event handling in agent-driven browser scenarios

Why agent-cdp?

The problem with bubus in agent browser scenarios

bubus is the event bus behind browser-use. It works well for simple single-agent flows, but has fundamental limitations when scaling to multi-agent, multi-tab scenarios:

LimitationImpact
Queued-only dispatchAll events go through an asyncio queue. CDP events that need instant response (popups, crashes, downloads) are forced to bypass bubus entirely via direct CDP callbacks — creating a dual-track event system
No propagation controlEvery handler always runs. A security watchdog can't consume() a navigation event to prevent subsequent handlers from executing — it can only raise ValueError as a hack
No handler priorityHandlers run in FIFO registration order. Security checks may execute after navigation has already started
No per-tab isolationSingle global event queue. All tabs share one dispatch path — no concurrent processing across tabs
No auto-disconnectWhen a tab closes, its handlers remain registered unless manually cleaned up
No event filtersCircuit-breaker logic is duplicated in every single handler wrapper (15 watchdogs x N handlers)

The problem with raw Playwright events

Playwright provides low-level page events (page.on('dialog'), page.on('response')) but no structured event system for agent coordination:

  • No event result aggregation (multiple handlers contributing partial state)
  • No priority-based handler ordering
  • No cross-tab event routing or fan-in/fan-out topologies
  • No awaitable events with timeout and deadlock detection
  • No event history or audit logging
  • Building agent watchdog coordination on top of Playwright events means reinventing most of what agent-cdp provides

What agent-cdp provides

agent-cdp combines the best of Qt's connection topology with bubus's domain-specific event model:

Source (EventScope) ──connect()──→ Handler    (Qt-style N:M connections)
  + per-Scope EventLoop for ordering           (bubus-style queued processing)

Features

Direct + Queued + Auto dispatch

from agent_cdp.connection import connect, ConnectionType

# Direct: zero-latency, runs in emit() call stack (sync)
connect(tab, NavigateToUrlEvent, security_check, mode=ConnectionType.DIRECT, priority=100)

# Queued: async, runs in scope's event loop
connect(tab, NavigateToUrlEvent, dom_rebuild, mode=ConnectionType.QUEUED, priority=0)

# Auto: same-scope → Direct, cross-scope → Queued
connect(tab, CrashEvent, crash_handler, mode=ConnectionType.AUTO)

No more bypassing the event system for time-critical handlers. Popup dismissal, crash recovery, and security checks all go through the same connection mechanism.

Event propagation control

def security_check(event: NavigateToUrlEvent) -> None:
    if not is_allowed(event.url):
        event.consume()  # stop propagation — navigation handler never runs
        raise NavigationBlocked(event.url)

connect(tab, NavigateToUrlEvent, security_check, mode=ConnectionType.DIRECT, priority=100)
connect(tab, NavigateToUrlEvent, do_navigate, mode=ConnectionType.DIRECT, priority=50)

High-priority Direct handler blocks the event before navigation starts — not after (as in bubus's "navigate then redirect to about:blank" pattern).

Per-scope isolation with concurrent dispatch

from agent_cdp.scope import EventScope, ScopeGroup

group = ScopeGroup('browser')
tab1 = await group.create_scope('tab-1', metadata={'target_id': '...'})
tab2 = await group.create_scope('tab-2', metadata={'target_id': '...'})

# Each scope has its own event loop — true concurrent processing
tab1.emit(NavigateToUrlEvent(url='https://site-a.com'))
tab2.emit(NavigateToUrlEvent(url='https://site-b.com'))
# Both process independently, no global queue bottleneck

N:M connection topology

# Fan-out: one source → many handlers
connect(tab1, NavEvent, security.check, mode=DIRECT, priority=100)
connect(tab1, NavEvent, dom.rebuild,    mode=QUEUED, priority=0)
connect(tab1, NavEvent, har.record,     mode=QUEUED, priority=-10)

# Fan-in: many sources → one handler
connect(tab1, NavEvent, monitor.on_nav, mode=QUEUED)
connect(tab2, NavEvent, monitor.on_nav, mode=QUEUED)
connect(tab3, NavEvent, monitor.on_nav, mode=QUEUED)

# Broadcast to all scopes
group.broadcast(CrashEvent(message='Chrome crashed'))

Generic typed events with result aggregation

from agent_cdp.events import BaseEvent, event_result, event_results_flat_dict

class ScreenshotEvent(BaseEvent[str]):  # result type = str (base64)
    full_page: bool = False

# Multiple handlers contribute partial state
event = tab.emit(BrowserStateRequestEvent())
await event  # wait for all handlers (Direct + Queued)
state = await event_results_flat_dict(event)
# {'dom_tree': ..., 'screenshot': ..., 'downloads': [...]}

Six aggregation modes: event_result, event_results_flat_dict, event_results_flat_list, event_results_by_handler_name, event_results_list, event_results_filtered.

Auto-disconnect on scope close

await group.close_scope('tab-1')
# → Event loop stopped
# → All outgoing connections severed (other scopes stop receiving)
# → All incoming connections severed (this scope stops receiving)
# → Handler references released for GC

No manual cleanup. No leaked handlers accumulating over browser reconnects.

Connection-level event filters

# Circuit breaker — one definition, applies to all handlers
tab.connect_all(circuit_breaker,
    mode=ConnectionType.DIRECT, priority=1000,
    filter=lambda e: type(e).__name__ not in LIFECYCLE_EVENTS)

Replace bubus's per-handler duplicated circuit-breaker wrappers with a single connection-level filter.

Race-free CDP setup with PausedTarget

from agent_cdp.bridge import PausedTarget, CDPEventBridge

# Guarantee: all bridges + handlers are wired BEFORE the target resumes
async with PausedTarget(resume=my_resume_fn):
    bridge = CDPEventBridge(cdp, scope, session_id='sess-A')
    bridge.bridge('Page.loadEventFired', lambda p: PageLoadEvent(**p))
    bridge.bridge('Page.javascriptDialogOpening', lambda p: DialogEvent(**p))
    scope.connect(PageLoadEvent, on_load, mode=ConnectionType.DIRECT)
    scope.connect(DialogEvent, on_dialog, mode=ConnectionType.DIRECT, priority=100)
# resume called automatically — even if setup raises an exception

# Or use the convenience factory:
async with CDPEventBridge.paused(resume=my_resume_fn):
    ...

No more race conditions between CDP event registration and page loading. PausedTarget coordinates the Stagehand V3 waitForDebuggerOnStart → pause → setup → resume pattern. Resume is idempotent and exception-safe.

Awaitable events + expect()

from agent_cdp.advanced import expect

# Events are awaitables — emit returns immediately, await for completion
event = tab.emit(NavigateToUrlEvent(url='https://example.com'))
await event  # waits for all Queued handlers

# Register before the trigger; no task scheduling or sleep is needed.
with expect(tab, NavigationCompleteEvent, timeout=30.0) as waiter:
    tab.emit(NavigateToUrlEvent(url='https://example.com'))
    complete = await waiter

Event logging with conscribe deserialization

from agent_cdp.advanced import EventLogWriter

writer = EventLogWriter(path='events.jsonl')
# Append completed events as JSONL with full type preservation
# Deserialize back using conscribe discriminated unions

Comparison

CapabilitybubusPlaywrightagent-cdp
Dispatch modesQueued onlyN/ADirect / Queued / Auto
Propagation controlNoneNoneevent.consume()
Handler priorityFIFO orderN/AInteger priority
Per-tab isolationShared queuePer-page eventsPer-scope event loops
Concurrent dispatchGlobal lockN/AIndependent per-scope
Connection topologyN:1:M (central bus)1:N (page events)N:M (direct connections)
Auto-disconnectNonePage close removes listenersscope.close() severs all
Event filtersAd-hoc circuit breakersNoneConnection-level filter
Result aggregation6 modesNone6 modes (preserved)
Typed eventsBaseEvent[T]UntypedBaseEvent[T] (preserved)
Event awaitingawait eventCallbacksawait event + expect()
Handler timeoutPer-handlerNonePer-handler + deadlock detection
Pre-resume hookNoneNonePausedTarget context manager
Event loggingJSONL WALNoneJSONL EventLog + conscribe
BroadcastEvent forwarding (shared ref)N/ADeep-copy broadcast
BackpressureUnbounded queueN/ABounded queue (default 1024)

Performance

Benchmarked on real websites (Google, Xiaohongshu, Bilibili, reCAPTCHA Demo) with 100 iterations per operation, same Chrome instance, GC disabled during measurement.

Framework overhead: negligible

OperationAvg latencyNotes
BaseEvent construction6.7 us (p50)Pydantic model + UUID7
emit() zero handlers3.6 us (p50)Connection resolution + empty loop
emit() 1 Direct no-op6.5 us (p50)Includes handler call + result recording
emit() 5 Direct handlers16.7 us (p50)Priority sorting + 5 handler invocations
emit() Direct + consume()5.9 us (p50)Early exit on propagation stop
emit() 1 Queued (enqueue only)9.4 us (p50)queue.put_nowait() + pending tracking
SecurityWatchdog (allowed URL)16.5 us (p50)Real handler: URL parse + domain check
SecurityWatchdog (blocked + raise)15.2 us (p50)consume() + exception propagation

Key insight: agent-cdp framework overhead averages 43 us per emit — 0.0017% of end-to-end operation time (avg 2.54s). The bottleneck is always network I/O, never the event system.

Raw CDP vs agent-cdp vs Playwright

Three channels connected to the same Chrome instance. Identical JavaScript executed through all channels to isolate pure automation-layer overhead.

  • Raw CDP — direct WebSocket commands via minimal CDPClient
  • agent-cdp — emit(Event) → Queued handler → CDP WebSocket → await event_result()
  • Playwright — page.evaluate() / page.screenshot() / high-level API
OperationRaw CDP (p50)agent-cdp (p50)Playwright (p50)acdp/CDPPW/acdp
JS evaluate (title)1.24 ms1.51 ms3.79 ms1.22x2.51x
JS evaluate (links)1.41 ms1.38 ms3.43 ms0.98x2.49x
DOM querySelector h13.78 ms4.71 ms13.44 ms1.25x2.85x
DOM querySelectorAll a2.57 ms3.69 ms8.24 ms1.44x2.23x
querySelectorAll (436-node page)5.26 ms5.16 ms88.60 ms0.98x17.17x
Screenshot (PNG)48.21 ms47.01 ms66.31 ms0.98x1.41x
DOMSnapshot + styles1.47 ms1.47 ms3.64 ms1.00x2.48x
Accessibility tree2.05 ms2.10 ms4.27 ms1.02x2.03x
Full cleaning pipeline (5 evals)7.17 ms8.02 ms18.47 ms1.12x2.30x

acdp/CDP ≈ 1.0x — agent-cdp's ~40us event dispatch overhead is negligible on millisecond-scale CDP operations.

PW/acdp = 2–17x — Playwright's protocol overhead (IPC, ElementHandle wrapping) compounds on every call.

CategoryRaw CDPagent-cdpPlaywrightacdp/CDPPW/acdp
JS eval (5 ops)1.64s1.73s3.89s1.05x2.25x
DOM API (querySelector)1.64s1.90s12.26s1.16x6.45x
Content (HTML)434 ms464 ms813 ms1.07x1.75x
Binary (screenshot)11.19s10.96s15.13s0.98x1.38x
Specialized (snapshot + a11y)1.75s1.89s3.90s1.08x2.06x
Pipeline (5-step cleaning)1.76s1.56s3.68s0.89x2.36x

DOM API operations show the largest gap because Playwright wraps each element in an ElementHandle with IPC overhead, while agent-cdp (like raw CDP) operates on raw nodeId integers.

Reproduce: uv run python -m demo.bench_cdp_vs_pw (Raw CDP vs PW) and uv run python -m demo.bench_agentcdp_vs_pw (agent-cdp vs PW)

Real-website benchmark: agent-cdp vs Playwright

Tested on production websites with complex DOM structures. 50 iterations per operation, same Chrome instance.

SiteDOM nodesDepthagent-cdp totalPW totalPW/acdp
Amazon50142733.73s75.67s2.24x
Xiaohongshu23562827.93s34.37s1.23x
Bilibili26502121.64s37.27s1.72x
Google594194.02s16.67s4.15x

Per-operation breakdown (p50 values):

OperationAmazon acdpAmazon PWXHS acdpXHS PWBilibili acdpBilibili PWGoogle acdpGoogle PW
get_html104 ms126 ms66 ms81 ms33 ms40 ms26 ms34 ms
eval_title1.8 ms3.9 ms2.0 ms4.0 ms2.0 ms4.4 ms1.8 ms4.0 ms
eval_links6.3 ms16.2 ms2.1 ms7.7 ms3.8 ms9.7 ms2.2 ms4.4 ms
eval_dom_stats3.2 ms5.7 ms2.7 ms5.8 ms3.2 ms5.9 ms1.8 ms3.5 ms
eval_interactive7.4 ms24.1 ms3.2 ms8.9 ms4.5 ms11.1 ms2.4 ms6.0 ms
query_h14.0 ms4.5 ms3.1 ms4.4 ms3.3 ms4.7 ms3.2 ms4.4 ms
querySelectorAll a30.6 ms505 ms11.4 ms171 ms11.3 ms226 ms5.8 ms31.8 ms
screenshot159 ms218 ms294 ms311 ms296 ms346 ms48 ms70 ms
dom_snapshot72 ms127 ms45 ms67 ms24 ms45 ms16 ms25 ms
accessibility_tree66 ms154 ms18 ms38 ms21 ms45 ms5.0 ms10 ms
cleaning_pipeline121 ms173 ms69 ms96 ms44 ms73 ms34 ms50 ms

Category totals across all 4 real websites:

Categoryagent-cdpPlaywrightPW/acdp
JS eval (5 ops)3.22s7.35s2.28x
DOM API (querySelector)3.91s49.05s12.56x
Content (HTML)11.73s14.33s1.22x
Binary (screenshot)40.37s47.80s1.18x
Specialized (snapshot + a11y)13.89s25.77s1.85x
Pipeline (5-step cleaning)14.20s19.73s1.39x

Overall: Playwright is 1.88x slower than agent-cdp (87.33s vs 164.03s total across 4 sites × 12 ops × 50 iterations).

The DOM API gap scales with page complexity — Amazon's 5014-node DOM produces a 16.6x gap on querySelectorAll, while Google's 594-node DOM shows 4.8x. This is because Playwright creates an ElementHandle object per element with round-trip IPC, while agent-cdp returns raw nodeId arrays in a single CDP response.

Reproduce: uv run python -m demo.bench_agentcdp_vs_pw

Real-site watchdog latency

Tested on Google, Xiaohongshu, Bilibili, and reCAPTCHA Demo:

OperationLatencyMode
Security check (Direct handler)72–164 usDIRECT, priority=100
Popup auto-dismiss5.5–12.5 msCDP event → DIRECT handler
Screenshot (Queued handler)47–321 msQUEUED, depends on page complexity
CDP Page.navigate round-trip232–910 msRaw CDP command
Full page load (navigate + render)0.83–1.67 sEnd-to-end

Reproduce: uv run python -m demo.bench

Multi-tab parallel benchmark (v0.4.0)

Browser-level CDP (single WebSocket, the same URL cloud providers give you) → 4 tabs × 5 watchdogs each (Security, Popups, Screenshot, Captcha, Crash). Real-world heavy-DOM sites.

Sequential vs Parallel

ModeWall-clockDetails
Sequential (one tab at a time)15.69sEach tab waits for the previous
Parallel (all 4 tabs via asyncio.gather)6.78sAll tabs navigate concurrently
Parallelism speedup2.52xLimited by slowest tab (Xiaohongshu)

Per-site breakdown (5 watchdogs, browser-level CDP)

SiteSecurity checkCDP navigatePage loadScreenshotCaptcha scanTotal
Xiaohongshu97 us520 ms1.51s281 ms3.2 ms3.88s
Amazon87 us638 ms1.34s1.02s2.4 ms4.54s
Bilibili77 us207 ms943 ms240 ms3.3 ms2.94s
reCAPTCHA76 us632 ms2.03s68 ms2.8 ms4.33s

Framework overhead: 0.0007% of end-to-end time. The bottleneck is always network I/O, never the event system.

Tier 1 optimizations (v0.4.0)

OptimizationMetricSpeedup
Scope-level deadlock monitor (replaces per-handler asyncio.create_task)per-handler overhead23.5x (7.6µs → 0.3µs)
orjson (replaces stdlib json) dumps screenshot 496KBCDP serialization30.5x (1.14ms → 37µs)
orjson dumps DOM 76KBCDP serialization4.3x (302µs → 71µs)
orjson loads DOM 76KBCDP deserialization2.0x (248µs → 122µs)
MRO match cache (ABC-inspired positive + negative + version counter)emit storm 100x10-12µs/event (cached)

Reproduce: uv run python -m demo.bench_optimizations

Action dispatch: using agent-cdp as an agent action executor

agent-cdp is not only for browser→handler event flow. It works equally well for the reverse direction: agent→browser action dispatch with anti-detection, security gating, and result collection.

Architecture

Agent LLM decides: "click #submit-btn"
  ↓
scope.emit(ClickAction(selector='#submit-btn'))
  ↓ handlers execute by priority
  ├─ [DIRECT p=100] security_check     → allowed? consume() + raise if not
  ├─ [QUEUED p=50]  stealth_executor   → bezier mouse trajectory + CDP Input
  └─ [QUEUED p=0]   audit_logger       → async log, doesn't block agent
  ↓
result = (await event_results_by_handler_name(event))['stealth_executor']
# → ClickResult(coords=(450, 320), trajectory_points=25)

Defining action events

from pydantic import BaseModel
from agent_cdp.events import BaseEvent, EmitPolicy

class ClickResult(BaseModel):
    coords: tuple[float, float]
    trajectory_points: int

class ClickAction(BaseEvent[ClickResult]):
    """BaseEvent[ClickResult] declares what handlers should return."""
    selector: str = ''
    emit_policy: ClassVar[EmitPolicy] = EmitPolicy.FAIL_FAST  # security failure stops chain

Registering handlers

from agent_cdp.connection import ConnectionType

# Security gate — DIRECT = runs synchronously inside emit()
scope.connect(ClickAction, security_check, mode=ConnectionType.DIRECT, priority=100)

# Anti-detection executor — QUEUED = async, can await CDP calls
scope.connect(ClickAction, stealth_click,  mode=ConnectionType.QUEUED, priority=50)

# Audit log — QUEUED, lowest priority, doesn't block agent
scope.connect(ClickAction, audit_logger,   mode=ConnectionType.QUEUED, priority=0)

Emitting actions and collecting results

event = scope.emit(ClickAction(selector='#submit-btn'))
# DIRECT handlers already executed (security check passed)

await event  # wait for QUEUED handlers to complete

# 4 ways to get results:
from agent_cdp.events import event_result, event_results_list, event_results_by_handler_name

# 1. First successful result
r = await event_result(event)

# 2. All results as list
all_r = await event_results_list(event)

# 3. By handler function name (most useful for action dispatch)
by_name = await event_results_by_handler_name(event)
click_result = by_name['stealth_click']  # → ClickResult(...)

# 4. By connection ID (most precise)
er = event.event_results[conn.id]
er.result       # ClickResult(...)
er.status       # ResultStatus.COMPLETED
er.handler_name # 'stealth_click'
er.error        # None

Security gating with consume()

def security_check(event: ClickAction) -> ClickResult:
    if event.selector in BLOCKED_SELECTORS:
        event.consume()  # prevents stealth_click and audit_logger from running
        raise SecurityViolation(f'Blocked: {event.selector}')
    return ClickResult(coords=(0, 0), trajectory_points=0)

When consume() is called, emit() breaks out of the handler loop. No subsequent handlers execute — the stealth executor never sends CDP commands, the audit logger never records. The exception propagates to the caller.

Anti-detection mouse trajectory (real CDP)

async def stealth_click(event: ClickAction) -> ClickResult:
    # 1. Get element coordinates via CDP
    rect = await cdp.evaluate(f'document.querySelector("{event.selector}").getBoundingClientRect()')

    # 2. Generate bezier curve trajectory
    trajectory = bezier_trajectory(current_pos, (rect.x, rect.y), steps=25)

    # 3. Send real mouse events via CDP Input domain
    for x, y in trajectory:
        await cdp.send('Input.dispatchMouseEvent', {
            'type': 'mouseMoved', 'x': x, 'y': y
        })
        await asyncio.sleep(random.uniform(0.005, 0.02))

    # 4. Click with human-like press/release timing
    await cdp.send('Input.dispatchMouseEvent', {'type': 'mousePressed', ...})
    await asyncio.sleep(random.uniform(0.04, 0.10))
    await cdp.send('Input.dispatchMouseEvent', {'type': 'mouseReleased', ...})

    # 5. Return value is automatically recorded as the action result
    return ClickResult(coords=(rect.x, rect.y), trajectory_points=len(trajectory))

The handler's return value is automatically captured by agent-cdp into event.event_results. The agent retrieves it via await event + aggregation functions. No manual record_result() calls needed.

MRO matching for base action types

class BrowserAction(BaseEvent[ActionResult]):
    __abstract__ = True

class ClickAction(BrowserAction): ...
class TypeAction(BrowserAction): ...
class ScrollAction(BrowserAction): ...

# Register on base class — automatically matches all subclass events
scope.connect(BrowserAction, security_check, mode=ConnectionType.DIRECT, priority=100)
scope.connect(BrowserAction, audit_logger,   mode=ConnectionType.QUEUED, priority=0)

# Register specific executors per action type
scope.connect(ClickAction,  stealth_click,  mode=ConnectionType.QUEUED, priority=50)
scope.connect(TypeAction,   stealth_type,   mode=ConnectionType.QUEUED, priority=50)
scope.connect(ScrollAction, stealth_scroll, mode=ConnectionType.QUEUED, priority=50)

Demos: demo_nano.py (minimal 70 lines), demo_feedback.py (result collection), demo_real_xhs.py (real Chrome + xiaohongshu.com with stealth mouse trajectory)

Scope architecture: real-world validation

Three demos validate agent-cdp's scope advantages with real Chrome and CDP. All results below are from actual runs, not simulated.

Per-scope isolation (demo.main, demo.multi_tab)

Phase 7: Per-Tab Popup Isolation
  ✓ [Google] popup dismissed: "popup-isolation-test"
  ✓ Other tabs saw 0 popup events — per-scope isolation confirmed

A Page.javascriptDialogOpening CDP event on the Google tab triggers the popup handler only on that tab's scope. Bilibili, Xiaohongshu, and reCAPTCHA scopes see nothing — no event leaks across scopes.

Concurrent dispatch: 15x speedup over sequential (demo.advanced)

Phase 6: Concurrent Dispatch — 5 scopes in parallel
  ✓ Sequential: 12192ms
  ✓ Concurrent: 813ms
  ✓ Speedup: 15.0x (bubus global queue → always sequential)

5 tabs navigate simultaneously. Each scope has its own event loop — no global queue serialization. bubus's single asyncio.Queue forces all tabs to wait in line.

Auto-disconnect on scope close (demo.multi_tab, demo.advanced)

Phase 10: Tab Close → Auto-Disconnect
  ✓ tab-bilibili closed. Remaining: ['tab-google', 'tab-xiaohongshu', 'tab-recaptcha']
  ✓ Closed scope rejects emit: Cannot emit on closed scope 'tab-bilibili'
  ✓ [Google] still works after Bilibili closed (screenshot OK)

scope.close() stops new input, closes owned bridges/waiters, and drains or cancels accepted deliveries within its budget. It disconnects connections after work exits; check CloseResult.complete when handlers may ignore cancellation. Remaining scopes continue operating normally.

Fan-in / Fan-out / Cross-scope routing (demo.advanced)

PatternWhat happensbubus equivalent
Fan-in (N:1)connect_all_scopes: 1 call → 6 connectionsNot possible — single bus
Fan-out (1:N)1 event → 4 handlers (Direct + Queued + cross-scope)Partial — all handlers always run
Cross-scopeHandler runs in monitor's event loop, not emitter'sNot possible
FiltersHandler skipped entirely when filter=FalseAd-hoc circuit breakers
BroadcastDeep-copy to all scopes (315 us for 4 scopes)Shared ref (mutation leaks)

CaptchaWatchdog: DOM-based detection (demo.multi_tab)

Phase 6: CaptchaWatchdog Detection
  ✓ reCAPTCHA DETECTED on demo page (2.03ms)
    vendor: recaptcha
    sitekey: 6Le-wvkSAAAAAPBMRTvw...
    elements: 3, challenge visible: True
  ✓ [Google] no captcha (correct)
  ✓ [Bilibili] no captcha (correct)

Tab-specific QUEUED watchdog inspects DOM via CDP Runtime.evaluate — fires only on the reCAPTCHA tab's scope, not on others.

Parallel screenshots across tabs

Phase 9: Parallel Screenshots (all tabs)
  ✓ [Google] 33965B, [Bilibili] 1019642B, [Xiaohongshu] 914908B, [reCAPTCHA] 23714B
  ✓ All 4 screenshots captured in 658.99ms (parallel)

Reproduce all demos:

uv run python -m demo.main            # single-tab scope advantages
uv run python -m demo.multi_tab       # multi-tab isolation + captcha detection
uv run python -m demo.advanced        # 8 architectural advantages with 5 tabs
uv run python -m demo.paused_target   # PausedTarget race-free setup (Amazon + Xiaohongshu)

Installation

pip install "agent-cdp>=0.5.0"

Requires Python >= 3.11, conscribe >= 2.0.0 and < 3, and Pydantic >= 2.7 and < 3.

Quick start

import asyncio
from agent_cdp.events import BaseEvent
from agent_cdp.connection import connect, ConnectionType
from agent_cdp.scope import EventScope, ScopeGroup

# Define events
class NavigateEvent(BaseEvent[str]):
    url: str

class PageLoadedEvent(BaseEvent[None]):
    url: str

# Create scopes
group = ScopeGroup('browser')

async def main():
    tab = await group.create_scope('tab-1')

    # Direct handler: security check runs synchronously in emit()
    def security_check(event: NavigateEvent) -> None:
        if 'evil.com' in event.url:
            event.consume()
            raise ValueError(f'Blocked: {event.url}')

    # Queued handler: async navigation
    async def do_navigate(event: NavigateEvent) -> str:
        # ... perform CDP navigation ...
        return f'navigated to {event.url}'

    connect(tab, NavigateEvent, security_check, mode=ConnectionType.DIRECT, priority=100)
    connect(tab, NavigateEvent, do_navigate, mode=ConnectionType.QUEUED, priority=0)

    # Blocked — security_check consumes the event
    try:
        tab.emit(NavigateEvent(url='https://evil.com'))
    except ValueError as e:
        print(f'Blocked: {e}')

    # Allowed — flows through security check, then queued navigation
    event = tab.emit(NavigateEvent(url='https://example.com'))
    await event  # wait for queued handler

    await group.close_all()

asyncio.run(main())

Architecture

From Qt:
├── ConnectionType (Direct / Queued / Auto)
├── N:M connection topology (connect / disconnect)
├── Event propagation control (consume)
├── Handler priority (integer ordering)
├── Auto-disconnect (scope.close)
└── EmitPolicy (FAIL_FAST / COLLECT_ERRORS)

From bubus:
├── BaseEvent[T] generic typed events
├── 6 result aggregation modes
├── Awaitable events (await event)
├── expect() declarative future event waiting
├── Parent-child event tracing (event_parent_id)
├── Per-handler timeout + deadlock detection
└── EventLog persistence (JSONL)

New in agent-cdp:
├── EventScope (isolated event processing domain)
├── ScopeGroup (lifecycle management + broadcast)
├── Per-scope event loops (no global lock)
├── Deep-copy broadcast
├── MRO-based event matching
├── connect_all() catch-all
├── Bounded queue with per-delivery rejection results and statistics
├── Direct handler timing monitor (>100ms warning)
├── CDPEventBridge (CDP → EventScope bridging)
├── CDPCommandProtocol (structural type for CDP clients)
└── PausedTarget (race-free pause → setup → resume coordination)

Development

git clone https://github.com/QLYYLQ/agent-cdp.git
cd agent-cdp
uv sync
uv run pytest -vxs tests/          # run all tests
uv run ruff check src tests demo scripts
uv run ruff format --check src tests demo scripts
uv run pyright                       # type check (strict mode)

License

MIT

Downloads & popularity

Awaiting observations

No metric observations yet

Download counts and popularity will appear here as the collector receives them.

Versions 1

Collected releases
VersionPublishedDirect dependenciesOpen version
0.5.015 Oct 20266

Package advisories

0
No matching advisories collected

This describes our collected data; it is not a security assessment.

Repository

PENDING
qlyylq/agent-cdp

Shared repository data · GitHub

License detected in repository
Not collected

Metadata history

Meaningful changes
Current collected state Current

Observed since 20 Oct, 21:37 UTC

No superseded metadata states collected. Unchanged observations do not create new revisions.

Record identity

package:61c52d01714a9428ce0006d43477930aed95f80742af5c4e9059609d471c7e8a