How the `cdp()` Function Evaluates JavaScript Expressions in ego-lite: A Deep Dive into Chrome DevTools Protocol Integration

The cdp() function in ego-lite is a low-level bridge that sends raw Chrome DevTools Protocol (CDP) commands to the embedded browser, enabling JavaScript evaluation through the Runtime.evaluate method with automatic serialization and error handling.

The cdp() function serves as the foundation for all browser automation in ego-lite, a lightweight browser control library. Understanding how JavaScript expression evaluation works in ego-lite requires tracing the path from high-level APIs down to the raw CDP transport layer.

The cdp() Function: Raw CDP Command Sender

At the core of expression evaluation lies the cdp() helper in src/cdp-eval.ts. This function dispatches arbitrary CDP commands and returns their results.

export async function cdp(method, params: any = {}, sessionId = undefined) {
  const result = state.cdpOverride
    ? await state.cdpOverride(method, params, sessionId)
    : (await send({ method, params, session_id: sessionId })).result || {};
  // Mirror Network domain state when the default session is used
  if (!sessionId && (method === "Network.enable" || method === "Network.disable")) {
    state.networkDomainEnabled = method === "Network.enable";
  }
  return result;
}

Source: src/cdp-eval.ts#L12-L26

Key behaviors of cdp():

  • Test override support: If state.cdpOverride is set, it intercepts all calls for mocking or debugging
  • Direct transport: Otherwise, commands route through send() (defined in src/browser-runtime.ts)
  • Network state tracking: Automatically mirrors Network.enable/Network.disable calls in state.networkDomainEnabled

The evaluate() API: User-Facing JavaScript Execution

Most agent scripts call evaluate() rather than cdp() directly. This higher-level function handles function serialization, argument passing, and automatic IIFE wrapping.

export async function evaluate(pageFunction, arg = undefined) {
  // …constructs a JavaScript expression string…
  if (hasReturnStatement(expression) && !expression.trim().startsWith("(")) {
    expression = `(function(){${expression}})()`;
  }
  return runtimeEvaluate(expression, sessionId, true);
}

Source: src/cdp-eval.ts#L29-L65

evaluate() accepts two forms of input:

  • JavaScript functions: Stringified and invoked with serialized arguments
  • String expressions: Raw code executed in page context

For string expressions with return statements at the top level, ego-lite automatically wraps them in an immediately-invoked function expression (IIFE). This matches Playwright's behavior and prevents syntax errors when returning values.

The runtimeEvaluate() CDP Call

The actual browser execution happens in runtimeEvaluate(), which constructs and sends the final Runtime.evaluate command.

const response = await cdp(
  "Runtime.evaluate",
  { expression, returnByValue: true, awaitPromise },
  sessionId,
);
return runtimeValue(response, expression);

Source: src/cdp-eval.ts#L67-L82

Critical parameters passed to Runtime.evaluate:

  • returnByValue: true: Forces Chrome to serialize the result rather than returning a remote object reference
  • awaitPromise: Boolean controlling whether the runtime should await a returned Promise before resolving

The runtimeValue() Result Handler

After the browser responds, runtimeValue() transforms the CDP payload into a native JavaScript value.

const result = response.result || {};
const details = response.exceptionDetails;
if (details || result.subtype === "error") {
  // throws a descriptive error
}
if (Object.hasOwn(result, "value")) return result.value;
if (Object.hasOwn(result, "unserializableValue"))
  return decodeUnserializableJsValue(result.unserializableValue);
return null;

Source: src/cdp-eval.ts#L96-L115

The result handling path covers three cases:

  1. Exceptions: Surfaced as descriptive errors with stack traces
  2. Serializable values: Returned directly via result.value
  3. Unserializable values: Decoded through decodeUnserializableJsValue() for types like NaN, Infinity, and BigInt

Practical Code Examples

Using evaluate() with string expressions:

// Simple property access
const title = await evaluate('document.title');
// Returns: "My Page"

Passing functions with arguments:

// Function with serialized arguments
const sum = await evaluate((a, b) => a + b, [3, 7]);
// Returns: 10

Awaiting page-side Promises:

// Async evaluation with automatic promise handling
const result = await evaluate(async () => {
  const data = await fetch('/api/info').then(r => r.json());
  return data.total;
});
// Returns: resolved value from the page

How Session IDs Enable Multi-Target Evaluation

When evaluate() detects a target ID passed as the optional second argument, it automatically attaches a temporary CDP session via Target.attachToTarget. This enables script execution in specific iframes, workers, or spawned tabs without requiring manual session management.

The sessionId flows through the entire chain: evaluate()runtimeEvaluate()cdp()send(), ensuring commands route to the correct browser execution context.

Key Source Files

File Responsibility
[src/cdp-eval.ts](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/cdp-eval.ts) Core implementation: cdp, evaluate, runtimeEvaluate, runtimeValue
[src/state.ts](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/state.ts) Holds cdpOverride and networkDomainEnabled flags
[src/helpers.ts](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/helpers.ts) Exports evaluate and js alias for agent consumption
[src/browser-runtime.ts](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/browser-runtime.ts) WebSocket/CDP transport layer implementing send()

Summary

  • cdp() is the transport-layer primitive that sends raw CDP commands through send() or a test override
  • evaluate() provides ergonomic JavaScript execution with automatic function serialization and IIFE wrapping
  • runtimeEvaluate() bridges to Chrome's Runtime.evaluate with returnByValue: true for serialized results
  • runtimeValue() converts CDP response payloads into native JavaScript values, handling exceptions and unserializable types

Frequently Asked Questions

What happens if JavaScript execution throws an error in the browser?

ego-lite checks response.exceptionDetails and result.subtype === "error" in runtimeValue(), throwing a descriptive error containing the original stack trace from the browser. This ensures page-side failures surface immediately in your Node.js code.

Can I evaluate code in a specific iframe or worker?

Yes. Pass a target ID as the second argument to evaluate(). ego-lite automatically calls Target.attachToTarget to create a session, executes your code in that context, and cleans up afterward.

What's the difference between evaluate() and calling cdp("Runtime.evaluate", ...) directly?

evaluate() handles JavaScript function serialization, argument passing, automatic IIFE wrapping, and result decoding. Direct cdp() calls require you to construct raw CDP parameters and manually process the Runtime.RemoteObject response.

How does ego-lite handle unserializable values like NaN or BigInt?

When Chrome cannot serialize a value through standard JSON, it returns an unserializableValue string. ego-lite's decodeUnserializableJsValue() function parses these special strings back into proper JavaScript values.

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 →