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:
- Builds the helper context by calling
helpers.helperContext(), which constructs the map of public APIs - Wraps every helper in a
wrapReadydecorator (lines 64-68) that queues calls until the optionalreadysignal resolves, ensuring the runtime connection is active before commands execute - Buffers console output by overwriting
console.logwith a buffered sink (lines 80-86) so agent output can be flushed asynchronously - Attaches to the global
egoobject if it exists, storing helpers attarget.ego.helpersand wrapping native methods likecreateTabanduseTaskSpaceexactly once using theEGO_WRAPPEDsymbol (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 likegoto,locator,waitFor, andscreenshotcall low-level drivers (e.g.,nav.goto,pointer.click) that eventually invokecdp()fromsrc/cdp-eval.tsbrowser: Tab management functions (listTabs,switchTab,openOrReuseTab) forward tosrc/driver/nav.tstaskSpaces: Lifecycle helpers for creation and claiming interact directly with the nativeegoAPI exposed viaexposeEgoMethodsinsrc/index.tscdp: Provides direct access to the raw CDP request function defined insrc/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:
- Response resolution: Messages containing an
idfield resolve the matching promise from thependingmap - Page event handling: Specific events like
Page.javascriptDialogOpeningupdate internal state maps (pendingDialogs) - Subscriber notifications: Registered listeners in
eventSubscribersreceive their subscribed events - 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
Navigating to a URL
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 createLocator → internalSelector 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 viainvalidateSession()when CDP signals session loss - All CDP traffic flows through
browserCdp()→rawCdp()→ego.sendCDPMessage, with responses resolved via thependingpromise map inhandleMessage() - The architecture maintains synchronous-looking APIs through wrappers like
wrapReadywhile 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →