How Data Flows Through ego-lite: Inside the 3-Layer CDP Bridge Architecture

ego-lite routes all browser automation commands through a three-layer stack—SDK installation, helper facades, and a runtime CDP core—that transforms high-level Playwright-style calls into raw Chrome DevTools Protocol messages and back.

The ego-lite browser automation runtime, maintained in the citrolabs/ego-lite repository, implements a unique data flow architecture that bridges user scripts with the Chrome DevTools Protocol (CDP). Unlike traditional browser automation libraries that bundle their own browser binaries, ego-lite operates as a thin client that communicates with a closed-source ego runtime through a tightly controlled message-passing interface. Understanding this data flow requires examining how commands traverse from the SDK surface down to the native CDP layer and how responses bubble back up through session management and event buffering.

The Three-Layer Architecture

The data flow splits cleanly into three architectural layers, each responsible for a specific transformation of the automation command lifecycle:

Layer Responsibility Primary Source File
1. SDK Installation Exposes public helpers (page, browser, taskSpaces) on the global object and wraps them for runtime readiness [src/index.ts](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/index.ts#L44-L81)
2. Helper Facade Builds high-level "Playwright-style" facades that translate user calls into low-level CDP requests [src/helpers.ts](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts#L222-L235)
3. Runtime Core Manages CDP sessions, sends raw messages, buffers events, and handles session loss with automatic re-attachment [src/browser-runtime.ts](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts#L7-L46)

Layer 1: SDK Installation and Global Exposure

When the module initializes via installEgoSdk() (lines 44-81 in src/index.ts), the runtime performs four critical wiring operations that establish the data flow foundation:

  1. Builds the helper context by calling helpers.helperContext(), which constructs the map of public APIs
  2. Wraps every helper in a wrapReady decorator (lines 64-68) that queues calls until the optional ready signal resolves, ensuring the runtime connection is active before commands execute
  3. Buffers console output by overwriting console.log with a buffered sink (lines 80-86) so agent output can be flushed asynchronously
  4. Attaches to the global ego object if it exists, storing helpers at target.ego.helpers and wrapping native methods like createTab and useTaskSpace exactly once using the EGO_WRAPPED symbol (lines 101-112)

This installation happens once per process and gives user code a synchronous-looking API while the underlying operations remain fully asynchronous.

Layer 2: Helper Facade Translation

The helperContext() function (lines 222-235 in src/helpers.ts) creates the SDK's public surface area. Each facade method acts as a thin adapter that transforms arguments and forwards them to the runtime core:

  • page: Methods like goto, locator, waitFor, and screenshot call low-level drivers (e.g., nav.goto, pointer.click) that eventually invoke cdp() from src/cdp-eval.ts
  • browser: Tab management functions (listTabs, switchTab, openOrReuseTab) forward to src/driver/nav.ts
  • taskSpaces: Lifecycle helpers for creation and claiming interact directly with the native ego API exposed via exposeEgoMethods in src/index.ts
  • cdp: Provides direct access to the raw CDP request function defined in src/cdp-eval.ts

Each helper method follows a consistent pattern: transform arguments into CDP-compatible payloads, then delegate to cdp(), which forwards to browserCdp() in the runtime core.

Layer 3: Runtime Core and Session Management

The heart of the ego-lite data flow lives in src/browser-runtime.ts, where three functions orchestrate the actual wire protocol communication:

ensureSession() (lines 107-144) manages connection state with a 2000ms TTL cache. It checks state.sessionId freshness, lists available tabs via ego.listTabs(), and attaches to the target using Target.attachToTarget. The resulting sessionId is stored in state.sessionId for reuse.

rawCdp() (lines 38-77) constructs the JSON payload (including the optional sessionId) and transmits it through ego.sendCDPMessage. It registers a promise in the pending map keyed by request ID, enabling asynchronous response resolution.

browserCdp() (lines 79-106) serves as the entry point for all helpers. It checks for test overrides in state.cdpOverride, auto-injects session IDs for page-level methods (lines 91-93), and implements resilient error handling. When "session lost" errors occur, it calls invalidateSession() and retries with a fresh session (lines 96-102).

Event Handling and Message Plumbing

Incoming CDP traffic is processed by handleMessage() (lines 32-88 in src/browser-runtime.ts), which implements a multi-stage dispatch:

  1. Response resolution: Messages containing an id field resolve the matching promise from the pending map
  2. Page event handling: Specific events like Page.javascriptDialogOpening update internal state maps (pendingDialogs)
  3. Subscriber notifications: Registered listeners in eventSubscribers receive their subscribed events
  4. Event buffering: All remaining events are stored in a capped buffer (10,000 entries) for later consumption

The state.ts file (lines 24-38) maintains mutable runtime state including the current sessionId, default timeouts, the active send function (defaulting to browserCdp), and helper overrides used by the test harness.

End-to-End Data Flow Example

When a user script executes await page.goto('https://example.com'), the data flows through the complete stack:


user script ──► page.goto (helpers.ts L222+)
                ► nav.goto (driver/nav.ts)
                ► cdp("Page.navigate", …) (cdp-eval.ts)
                ► browserCdp() (browser-runtime.ts L79)
                ► ensureSession() (L107) → attaches to target
                ► rawCdp() (L38) → ego.sendCDPMessage
                ► ego runtime (performs navigation)
                ◄─ CDP response ─ handleMessage() resolves
                ◄─ promise resolves to user script

Each layer abstracts the asynchronous complexity while preserving error recovery capabilities. The wrapReady decorator in layer 1 ensures the runtime is active, layer 2 translates the high-level intent into a CDP command, and layer 3 manages the wire protocol, session validity, and retry logic.

Practical Code Examples

await page.goto('https://example.com');        // → nav.goto → cdp("Page.navigate")
console.log(await page.title());               // → cdp("Runtime.evaluate")

Underlying flow: page.goto (helpers) → nav.goto (driver) → cdp() (cdp-eval) → browserCdp() (runtime) → rawCdp() (ego.sendCDPMessage).

Clicking with a Locator

const btn = page.locator('button[data-action="submit"]');
await btn.click();                             // → pointer.click → cdp("Input.dispatchMouseEvent")

The locator string is built via createLocatorinternalSelector in src/helpers.ts and passed to the CDP driver, which resolves it to a DOM node ID before dispatching the mouse event.

Using Task Spaces

const ts = await taskSpaces.useOrCreate('my-space');
await ts.switch(ts.id);                        // → ego.useTaskSpace
await page.goto('https://example.org');
await taskSpaces.complete(ts.id, { keep: false }); // → ego.closeTaskSpace

Executing Site-Specific Tools

const result = await site.runTool('github', 'searchIssues', {
  query: 'bug',
});

site.runTool loads the Node-side tool source and executes it within the same helper context, allowing the tool to call any high-level facade (page, browser, etc.) just like standard user code.

Summary

  • ego-lite implements a three-layer data flow: SDK installation (src/index.ts), helper facades (src/helpers.ts), and runtime CDP core (src/browser-runtime.ts)
  • The installEgoSdk() function wraps all helpers with readiness guards and buffers console output before exposing APIs globally
  • Session management uses a 2000ms TTL cache in ensureSession(), with automatic re-attachment via invalidateSession() when CDP signals session loss
  • All CDP traffic flows through browserCdp()rawCdp()ego.sendCDPMessage, with responses resolved via the pending promise map in handleMessage()
  • The architecture maintains synchronous-looking APIs through wrappers like wrapReady while handling asynchronous wire protocol communication, retries, and event buffering internally

Frequently Asked Questions

How does ego-lite handle session loss during automation?

The runtime detects session loss errors in browserCdp() (lines 96-102 of src/browser-runtime.ts) and automatically invalidates the cached session via invalidateSession(). It then retries the CDP command with a fresh session acquired through ensureSession(), which re-attaches to the target tab using Target.attachToTarget. This retry mechanism is transparent to user scripts.

What is the role of the helper facade in the data flow?

The helper facade, constructed by helperContext() in src/helpers.ts (lines 222-235), acts as a translation layer between user-friendly APIs and raw CDP commands. It transforms high-level concepts like "click this locator" into the specific CDP domains (Input.dispatchMouseEvent, DOM.querySelector, etc.) and manages argument serialization before delegating to the runtime core.

How are CDP events buffered and consumed in ego-lite?

Incoming CDP events are processed by handleMessage() in src/browser-runtime.ts, which stores all non-resolved events in a buffer capped at 10,000 entries. Subscribed listeners receive their specific events immediately, while page-level events (like dialog openings) update internal state maps. User code can access this buffer through the SDK's event inspection APIs to consume historical CDP traffic.

What happens when I call page.goto() in an ego-lite script?

The call traverses four stages: first, page.goto in src/helpers.ts calls the navigation driver; second, nav.goto in src/driver/nav.ts builds the CDP payload; third, cdp() in src/cdp-eval.ts forwards to browserCdp(); fourth, src/browser-runtime.ts ensures a valid session exists (attaching if necessary) and transmits the command via ego.sendCDPMessage. The promise resolves when handleMessage() receives the CDP response with a matching request ID.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →