# How Ego-Lite Captures Snapshots and Page State: A Complete Technical Guide

> Explore how Ego-Lite captures page state with its three-stage pipeline. Understand snapshotting, ref-maps, and AI agent integration for detailed technical insights.

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

---

**Ego-Lite captures page state through a three-stage pipeline—`snapshotRaw()` fetches structured DOM data from the native runtime, `browserSnapshotRefsToRefMap()` builds a global ref-map for element resolution, and `snapshot()` returns a simplified text view for AI agents.**

The `citrolabs/ego-lite` browser automation framework bridges the gap between raw browser internals and semantic, LLM-friendly page representations. Its snapshot system enables AI agents to reason about web pages using stable references like `@23` instead of fragile CSS selectors. This article breaks down the complete implementation, from the native runtime call to the ref-based element interactions you use in agent code.

## The Three-Stage Snapshot Pipeline

Ego-Lite's snapshot and page state capture works through three tightly-coupled stages defined in [`driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/driver/observe.ts).

### Stage 1: Request Raw Snapshot Data

The internal helper `snapshotRaw()` initiates capture by calling the native ego runtime through `browserEgo().snapshot(options)`:

```typescript
// driver/observe.ts, lines 49-63
export async function snapshotRaw(options?: SnapshotOptions): Promise<SnapshotResult> {
  const result = await browserEgo().snapshot({
    scope: options?.scope ?? "full_page",
    includeActionMarks: options?.includeActionMarks ?? true,
    includeStableLocator: options?.includeStableLocator ?? true,
  });
  // result contains {content: string, refs: SnapshotRef[]}
  return result;
}

```

The runtime returns a **structured object** with two critical properties:

- **`content`** — The rendered, accessible text representation of the page
- **`refs`** — An array of snapshot references like `@23` that map to backend node IDs

Each ref is a short, stable identifier that survives DOM mutations better than CSS selectors.

### Stage 2: Refresh the Internal Ref-Map

Once the raw snapshot arrives, the system populates the **global ref-map** (`browserRefMap`) to enable future element resolution:

```typescript
// driver/observe.ts, lines 61-66
browserSnapshotRefsToRefMap(browserRefMap, result.refs || []);
registerSnapshotForRefRefresh(() => snapshotRaw());

```

This two-step update mechanism ensures:

1. **Immediate mapping** — Each `@N` ref is tied to its backend node ID and selector metadata
2. **Automatic refresh** — Future ref lookups trigger fresh snapshots if the map becomes stale

The ref-map lives in [`ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/ref-state.ts) and persists across agent actions, making `await click("@23")` possible even after minor page changes.

### Stage 3: Deliver the Convenient Text View

The public `snapshot()` helper wraps `snapshotRaw()` with opinionated defaults and strips the structured metadata:

```typescript
// driver/observe.ts, lines 73-80
export async function snapshot(options?: SnapshotOptions): Promise<string> {
  const result = await snapshotRaw({
    scope: options?.scope ?? "full_page",
    includeActionMarks: options?.includeActionMarks ?? true,
    includeStableLocator: options?.includeStableLocator ?? true,
  });
  return result.content;  // Only the text surface
}

```

**`snapshot()`** is what most agents call (`await page.snapshot()`). It returns clean, LLM-ready text without exposing implementation details.

**`snapshotRaw()`** is reserved for advanced use cases where you need the full `{content, refs}` object or custom options.

## How the Ref-Map Enables Element Resolution

The ref-map system—spanning [`ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/ref-state.ts), [`ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/ref-map.ts), and [`element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/element-resolver.ts)—translates snapshot references into executable element handles.

### Core Components

| Component | File | Responsibility |
|-----------|------|----------------|
| `browserRefMap` | [`src/ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-state.ts) | Singleton `Map<number, RefInfo>` holding all active refs |
| `browserSnapshotRefsToRefMap()` | [`src/ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-state.ts) | Populates the map from snapshot results |
| Lookup utilities | [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts) | Resolves refs, triggers refresh on stale lookups |
| Element resolution | [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) | Converts refs to executable handles for click, evaluate, etc. |

### Resolution Flow

When you call `await click("@23")`, the resolver:

1. Checks `browserRefMap` for entry `23`
2. If missing or stale, triggers `snapshotRaw()` via the registered refresh hook
3. Retrieves the backend node ID from the refreshed map
4. Executes the action through [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts)

This indirection insulates agent code from DOM volatility.

## Snapshot Options and Page State Granularity

The `SnapshotOptions` interface controls how the ego runtime serializes page state:

| Option | Values | Effect on Page State Capture |
|--------|--------|------------------------------|
| `scope` | `"full_page"` (default) | Captures entire scrollable document |
| `scope` | `"only_within_viewport"` | Limits to currently visible area |
| `includeActionMarks` | `true` (default) | Annotates interactable elements with action hints |
| `includeStableLocator` | `true` (default) | Computes durable CSS/XPath selectors for reuse |

The ego runtime (closed-source binary) performs the heavy lifting: DOM serialization, accessible name computation, and optional annotation injection.

## Practical Code Examples

### Basic Text Snapshot

```javascript
// Standard agent pattern—clean text for LLM consumption
const pageText = await page.snapshot();
console.log(pageText);
// Output: semantic text with @23-style refs embedded for key elements

```

Uses defaults: `full_page`, action marks enabled, stable locators enabled.

### Structured Raw Snapshot

```javascript
// Access refs and stable locators directly
const raw = await page.snapshotRaw({
  scope: "only_within_viewport",
  includeActionMarks: false,
  includeStableLocator: true,
});

console.log(raw.content);  // Visible text only
console.log(raw.refs);
// [{ref: "@12", nodeId: 12345, stableLocator: "css=button.submit", ...}, ...]

```

### Ref-Based Element Interaction

```javascript
// Snapshot, find element by content, click by ref
const snap = await page.snapshotRaw();
const targetRef = snap.refs.find(r => r.text.includes("Submit"))?.ref; // "@7"
await page.click(targetRef);  // Resolver handles the @7 → nodeId translation

```

### Manual Ref-Map Refresh

```javascript
// Force synchronization (rarely needed—automatic on stale access)
await page.snapshot();  // Fresh snapshot; ref-map updated automatically

```

## Key Source Files

| File | Role |
|------|------|
| [`src/driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/observe.ts) | `snapshotRaw()`, `snapshot()`, refresh hook registration |
| [`src/ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-state.ts) | `browserRefMap` singleton, `browserSnapshotRefsToRefMap()` |
| [`src/ref-map.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/ref-map.ts) | Ref lookup utilities, freshness checking |
| [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts) | Bridge to native `browserEgo().snapshot()` |
| [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) | Exposes `snapshot`/`snapshotRaw` to agent globals |
| [`src/format.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/format.ts) | Public API type definitions |
| [`src/element-resolver.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/element-resolver.ts) | Ref-to-handle resolution for actions |

## Summary

- **`snapshotRaw()`** in [`driver/observe.ts`](https://github.com/citrolabs/ego-lite/blob/main/driver/observe.ts) calls the native ego runtime and returns structured `{content, refs}` data
- **`browserSnapshotRefsToRefMap()`** in [`ref-state.ts`](https://github.com/citrolabs/ego-lite/blob/main/ref-state.ts) builds the global ref-map that powers stable element references
- **`snapshot()`** provides the simplified text view most agents consume, with sensible defaults for scope and annotations
- **The ref-map system** enables durable `@N` references that survive DOM changes better than raw selectors
- **Snapshot options** control granularity—viewport vs. full page, action marks, and stable locator computation

## Frequently Asked Questions

### What is the difference between `snapshot()` and `snapshotRaw()`?

**`snapshot()`** returns a plain string of page content optimized for LLM prompts. **`snapshotRaw()`** returns the full structured object including the `refs` array and stable locators. Use `snapshotRaw()` when you need to resolve element references programmatically before acting.

### How does the `@23` ref system work under the hood?

Each `@N` reference maps to a backend node ID stored in `browserRefMap`. When you pass `"@23"` to `click()` or `elementCenter()`, the resolver looks up node ID `23`, validates freshness, refreshes the snapshot if needed, and executes against the corresponding DOM node.

### Can I capture only the visible viewport instead of the full page?

Yes. Pass `scope: "only_within_viewport"` to `snapshotRaw()` or `snapshot()`. This reduces payload size and processing time for large documents when you only need immediately visible content.

### What happens if a ref becomes stale between snapshot and action?

The ref-map automatically triggers `snapshotRaw()` via the registered refresh hook in `registerSnapshotForRefRefresh()`. This ensures the map stays consistent without manual intervention, though it adds one snapshot latency on first stale access.

### Where is the actual DOM serialization implemented?

The heavy lifting occurs in the **closed-source ego runtime binary**, invoked through `browserEgo().snapshot()` in [`browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/browser-runtime.ts). The open-source TypeScript layer handles option marshaling, ref-map maintenance, and convenient API surfacing.