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

> Explore how ego-lite's 3-layer CDP bridge architecture routes browser automation commands. Learn how Playwright-style calls transform into raw Chrome DevTools Protocol messages and back.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: internals
- Published: 2026-08-23

---

**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/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/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/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/src/cdp-eval.ts)
- **`browser`**: Tab management functions (`listTabs`, `switchTab`, `openOrReuseTab`) forward to [`src/driver/nav.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/src/index.ts)
- **`cdp`**: Provides direct access to the raw CDP request function defined in [`src/cdp-eval.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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

```javascript
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

```javascript
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`](https://github.com/citrolabs/ego-lite/blob/main/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

```javascript
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

```javascript
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`](https://github.com/citrolabs/ego-lite/blob/main/src/index.ts)), helper facades ([`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts)), and runtime CDP core ([`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) calls the navigation driver; second, `nav.goto` in [`src/driver/nav.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/nav.ts) builds the CDP payload; third, `cdp()` in [`src/cdp-eval.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/cdp-eval.ts) forwards to `browserCdp()`; fourth, [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/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.