# How to Trace the Data Flow from Agent JavaScript to Browser Runtime in Ego-Lite

> Trace the data flow from agent JavaScript to browser runtime in ego-lite. Understand how agent code executes and communicates with the browser via CDP commands.

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

---

**TLDR:** In `citrolabs/ego-lite`, agent-provided JavaScript is read by `runMain()` ([`src/run.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/run.ts)), wrapped in an async function with a helper façade, and executed—where helper calls transform into CDP commands sent through the `globalThis.ego` bridge to the browser runtime via [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts).

The `citrolabs/ego-lite` repository enables AI agents to control a browser by executing raw JavaScript snippets. Understanding the **ego-lite data flow from agent JavaScript to browser runtime** is critical for developers building autonomous browser agents or contributing to the project. This article traces the exact pipeline through the source code, explaining how code transforms from a CLI string into real browser actions like navigation, clicks, and DOM queries.

## The Ego-Lite Data Pipeline: An Overview

The architecture of ego-browser consists of a strict sequence of modules that translate user intent into browser actions. By mapping out the **agent JavaScript to browser runtime pipeline**, there is absolute clarity on how state flows through the system.

1. **CLI Entry Point** → `runMain()` reads the heredoc script.
2. **Context Construction** → `executionContext()` calls `helperContext()` to build the facade.
3. **Global Injection** → The facade is assigned to `globalThis`.
4. **Code Execution** → An `AsyncFunction` wraps the user code and runs it.
5. **CDP Transliteration** → Helpers call `cdp()` and `evaluate()`.
6. **Bridge Communication** → `send()` passes the command through `globalThis.ego`.
7. **Browser Runtime** → [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts) processes the session and executes actions.
8. **Result Decoding** → CDP responses are unwrapped and returned to the agent script.
9. **Output Flushing** → `flushSink()` writes console logs to stdout.

## Step 1: The CLI Reads the Agent Script ([`src/run.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/run.ts))

Every data flow begins in the `runMain()` function inside [`src/run.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/run.ts). The CLI reads the JavaScript snippet either from **stdin** or from the `stdinText` option. This is the entry point where the raw agent JavaScript enters the ego-lite system.

```typescript
// Simplified from src/run.ts
const code = process.env.QUIET_MODE 
  ? readStdin() 
  : browserConfig.stdinText;

```

This raw snippet becomes the foundation for the entire execution context.

## Step 2: Building the Execution Context and Helper Façade ([`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts))

The **execution context** is created by calling `executionContext()` which invokes `helpers.helperContext(agentHelpers)`. According to the source in [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts), this is where the public façade objects (`page`, `browser`, `taskSpaces`, `site`, `fetch`, `cdp`, `evaluate`) are assembled.

These facade functions are not directly interacting with the browser. Instead, they are driver modules. For example, the page methods invoke `nav.goto`, `pointer.click`, and `keyboard.press` from the `src/driver/*` folder. This abstraction layer is critical for the **ego-lite browser runtime** to remain isolated from complex protocol logic.

## Step 3: Injecting Helpers into the Global Scope

`runMainCommand` takes the façade and assigns it to `globalThis`. This means the agent script does not require any imports to use `page`, `browser`, `taskSpaces`, etc. Inside the executed script, the agent can directly access `globalThis.page` and it will resolve to the helper façade.

## Step 4: Executing User Code with `AsyncFunction`

The raw string code is dynamic. To execute it, ego-lite uses the `AsyncFunction` constructor. The user code is wrapped in an async function whose parameters are the helper names. This instantiates the code within the correct scope context and allows it to `await` browser operations.

```javascript
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
const fn = new AsyncFunction(...Object.keys(agentHelpers), code);

```

This is the exact point where the **agent JavaScript to browser runtime execution flow** begins.

## Step 5: How Helpers Convert to CDP Commands

This core data flow consists of the helper calls invoking lower-level CDP wrappers.

### The `cdp()` and `evaluate()` Wrappers in [`src/cdp-eval.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/cdp-eval.ts)

In [`src/cdp-eval.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/cdp-eval.ts), the `cdp()` function sends the command via `send()` to the CDP bridge. The `evaluate()` function wraps `Runtime.evaluate` and handles decoding of unserializable values like `NaN`, `Infinity`, and negative zero.

```typescript
// Pseudo code based on src/cdp-eval.ts
import { send } from './state';

async function cdp(method: string, params: object) {
  const targetId = await getTargetId();
  const response = await send(targetId, method, params);
  return response;
}

```

### The Ego Bridge in [`src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/state.ts)

The bridge is established through `globalThis.ego`. When `cdp()` is invoked, it sends the serialized request to `state.cdpOverride` for test scenarios or to `send()` otherwise. This module is the actual internal-facing API to the embedded ego-lite browser binary. The binary then forwards these CDP commands to Chrome via Chrome DevTools Protocol.

## Step 5: Browser-Runtime Processing ([`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts))

Centralising on the browser side, [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) handles the lifecycle of all CDP sessions. It manages:

- **Session ID Caching** – reuse a single session across multiple actions.
- **Event Buffering** – maintains a queue for browser events.
- **Actions** – implements higher-level operations including snapshots, element resolution, and task-space management.

This module is the actual "browser runtime" referenced in your question. It processes the raw CDP commands and turns them into physical browser state changes.

## Step 6: Handling the Result and Decoding Values

After the runtime executes, the result flows back stage by stage.

- `cdp()` resolves to the raw CDP result.
- `evaluate()` handles serialization edge cases (e.g., `NaN`, `Infinity`).
- Higher-level helpers translate protocol responses into friendly JavaScript values. For example, `page.url()` returns a simple string, or `locator.textContent()` returns the DOM node's string.

This reverse path ensures that the data flow from the browser runtime back to the agent script is accurate and type-safe.

## Step 7: Flushing Output to Stdout (`flushSink`)

As the agent script executes, any `console.log` calls are buffered via a specially-designed sink. After the script finishes, `flushSink()` writes the output to the CLI stdout. If the script threw an exception, the buffer is discarded, and the error propagates.

## Code Of Examples: Seeing the Data Flow in Action

The following script demonstrating the data flow:

```javascript
async function main() {
  await page.goto('https://example.com');   // nav.goto -> CDP('Page.navigate')
  await page.waitForLoadState();            // waits.waitForLoadState -> CDP('Page.loadEventFired')
  const title = await page.title();         // evaluate(() => document.title)
  console.log('Page title is', title);
}

```

```js
const loginBtn = page.locator('button#login');
await loginBtn.waitFor();                  
await loginBtn.click();                     

```

```js
const {result} = await cdp('Network.getCookies');
console.log('Cookies:', result);

```

## Key Source Files for the Ego-Lite Data Flow

| File | Role | Link |
|---|---|---|
| [`src/run.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/run.ts) | CLI entry point, reads script, builds execution context, runs code | [run.ts](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/run.ts) |
| [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) | Constructs the public facing façade (`page`, `browser`, etc.) | [helpers.ts](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts) |
| [`src/cdp-eval.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/cdp-eval.ts) | Low-level CDP wrapper (`cdp`, `evaluate`) and value decoding | [cdp-eval.ts](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/cdp-eval.ts) |
| [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) | Manages CDP session lifecycle, event buffering, snapshots | [browser-runtime.ts](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts) |
| [`src/state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/state.ts) | Manages global mutable runtime state; provides the ego bridge | [state.ts](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/state.ts) |
| `src/driver/*` | Implements concrete actions that call `cdp()` | [driver folder](https://github.com/citrolabs/ego-lite/tree/main/package/ego-browser/src/driver) |

## Summary

The data flow pipeline in **ego-lite** lets developers run agent JavaScript directly in the browser.

- The **agent JS is read** by `runMain()` and wrapped in an `AsyncFunction`.
- The **helper façade** (`page`, `locator`, `browser`) drives `cdp()` calls.
- The **bridge** inside global scope passes commands to the browser.
- The **`browser-runtime`** layer serializes and handles the raw browser actions.
- All responses flow back to the agent script efficiently using standard CDP serialization.
- A `console.log` obstacle, buffered and flushed, removes complexity from the CLI.

##  Frequently Asked Questions

### What is the entry point for agent JavaScript in ego-lite?

The entry point is the `runMainCommand()` function located in [`src/run.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/run.ts). It reads the JavaScript and then constructs the execution context to start the data flow.

### How do helpers like `page.goto()` translate into browser actions?

When JavaScript calls `page.goto()`, the helper translates it into a `cdp()` invocation with the `Page.navigate` method. The `cdp()` function sends this command via the `globalThis.ego` bridge, which communicates with the browser through the Chrome DevTools Protocol.

### What role does the [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) module play in the data flow?

The module is the central processing unit in the runtime for browser sessions. It manages CDP session IDs, buffers events, and handles high-level operations like element resolution and task spaces. During the "ego bridge" hands it an action, it performs the actual operation in the browser.

### How are results decoded and returned to the agent?

Results are decoded in `evaluate()` function of [`src/cdp-eval.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/cdp-eval.ts). It handles non-serializable values (like `NaN` or `Infinity`) and wraps the CDP response into friendly JavaScript values, ensuring the agent script receives data that is easily represented by standard JavaScript types. `flushSink` then writes the console output to the CLI.