Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .changeset/browser-session-guardrails.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
"agents": minor
---

`createBrowserSession` accepts `guardrails` — Browser Run [hostname guardrails](https://developers.cloudflare.com/browser-run/features/guardrails/) (`allowedDomains`, `allowedDomainSets`) sent in the session-acquire body and fixed for the session's lifetime, including Live View connections. `connectBrowserSession` accepts an options object (`{ timeoutMs, onClose, onActivity }`) in place of the bare timeout number — `onClose` runs once when the session reaches a terminal state (explicit close, peer closure, or socket error), and `onActivity` fires on every CDP command sent, as an activity signal for idle tracking; the numeric form still works but is now deprecated and will be removed. The `BrowserSessionGuardrails` and `ConnectBrowserSessionOptions` types are exported from `agents/browser`.

`ConnectBrowserOptions` is now an engine-discriminated union: `browser: "kitesurf"` removes the Chromium-only options (`keepAliveMs`, `includeTargets`, `recording`) at the type level, and `browser: "chromium"` is accepted explicitly on the default arm (the `ConnectChromiumBrowserOptions` and `ConnectKitesurfBrowserOptions` arms are exported). `CdpSession` now takes a `CdpSessionOptions` object (`{ timeoutMs, onClose, sessionId, onActivity }`); the positional constructor still works but is deprecated and will be removed.

**Migration** — only needed if you construct `CdpSession` directly around your own WebSocket. Move the positional arguments into the options object; `dispose` is renamed `onClose`:

```ts
// Before (deprecated)
new CdpSession(ws, 30_000, releaseBrowser, "session-1");

// After
new CdpSession(ws, {
timeoutMs: 30_000,
onClose: releaseBrowser,
sessionId: "session-1"
});
```

Sessions obtained from `connectBrowser` or `connectBrowserSession` are unaffected.
1 change: 1 addition & 0 deletions design/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ Keep it concise. A few paragraphs is fine. These are records, not essays.
| `rfc-streams.md` | RFC | Streams — durable incremental output as a Lifecycle capability: chunk log, cursor, replay-then-tail reads, composed with Tasks via checkpointed cursors (proposed) |
| `rfc-pi-harness-example.md` | RFC | Pi `AgentHarness` as an example-local Lifecycle capability over Tasks, Streams, and WebSockets; what must land before it becomes a package export (proposed) |
| `rfc-self-modifying-harness.md` | RFC | Editable harness source in Workspace, activation through Worker Bundler, and one fresh Dynamic Worker per turn (proposed) |
| `browser-sessions.md` | design doc | Internal named browser sessions — reattach-or-create session core, restart signaling, `BrowserSessions` lifecycle capability |

## Relationship to `/docs`

Expand Down
82 changes: 82 additions & 0 deletions design/browser-sessions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Browser Sessions

**Status:** internal (`session-core.ts`, `capability.ts` — not exported)

## Problem

Harness evaluations of the codemode browser surface ([browser-tools.md](./browser-tools.md)) showed the model spending most of its budget managing browser lifetime rather than doing its task: rediscovering and reattaching to tabs with `Target.getTargets` / `attachToTarget` in the majority of executions, deciding when to promote or close sessions, and failing when raw CDP session ids went stale across executions. Browser lifetime should be a host concern: each execution should run against one consistent browser, and the model should only hear about lifetime when its browser was replaced.

## How it works

Two internal layers, added additively. Neither is exported from `agents/browser`; `browser_execute`, quick actions, and the connector keep shipping unchanged. Raw CDP stays the model's interaction surface — these layers change who owns the browser, not how the model drives it.

### Named session core (`browser/session-core.ts`)

`NamedBrowserSessions` gives hosts named, reattach-or-create Browser Run sessions:

- Names are the identity; the model never sees platform session ids. Records live under a `browser:session:<name>` keyspace, distinct from the connector's `cdp:exec:` / `cdp:reuse:` entries.
- `resolve(name)` reattaches to a live session or creates a replacement, reporting `restarted: true` when the previous browser died, expired, or was closed — only first-ever use of a name reports `false`.
- Creation options (`keepAliveMs`, `recording`, `guardrails`) are durable: reapplied to every replacement session. `keep_alive` defaults to the 600-second platform maximum.
- There is no host-side sweep. Browser Run's `keep_alive` reclaims an idle browser; the next `resolve` finds it dead (404/410 on the liveness probe) and replaces it.
- `close`, and a `resolve` that finds its browser dead, retire the name under its lock: the record moves to a permanent `browser:retired:<name>` marker outside the named-session keyspace. The marker is what makes the next `resolve` report `restarted: true`, and it is read under the commit lock, so a name created and closed while another create is in flight still counts as used. `close` then deletes the Browser Run session best-effort; if that fails, `keep_alive` reclaims it.
- CDP commands on sockets from `connect`, reattaches, and Live View links refresh the record's `updatedAt`, throttled to once per 60 seconds (capped at half `keep_alive`). A refresh never recreates a record that was closed or replaced in the meantime.
- Store locks are held around storage reads and writes only — never across Browser Run network calls. Concurrent resolvers commit first-wins, and the loser deletes its redundant browser.
- `openOneShotBrowserSession` covers store-less create-and-close use, including Kitesurf (which is connection-scoped, so it is one-shot only). Engine options are discriminated unions: choosing `browser: "kitesurf"` removes the Chromium-only options (`guardrails`, `keepAliveMs`, `recording`) at the type level, backed by one runtime guard per entry point for plain-JS callers.

### `BrowserSessions` lifecycle capability (`browser/capability.ts`)

A `LifecycleCapability` (id `"browser"`) composable onto an `Agent` subclass or a plain Durable Object via `Lifecycle`:

- Auto-supplies a `DurableBrowserSessionStore` over the Durable Object's storage; hosts may pass their own store.
- Schedules nothing: no Lifecycle jobs, alarms, or hooks. An object with browser sessions is never woken on their behalf.
- Host-only observability: `sessions()` lists named sessions that currently own a browser, with timestamps and a `live` / `expired` status. `expired` means no recorded activity for a full `keep_alive` window, so the platform has most likely reclaimed the browser; it is a best guess, because a human driving the browser through Live View keeps it alive without updating the record. Closed names are not listed. `liveView(name, { mode })` creates fresh Live View URLs (valid ~5 minutes to connect, never persisted) for a live named session and returns `undefined` for absent, closed, or dead ones. Creating a link counts as activity, and a link that loses a race with `close` reports the session gone rather than returning dead URLs. The Live View helpers live in `browser/live-view.ts`, shared with the connector.

When `BrowserSessions` is exported (planned once the Lifecycle API firms up and the capability is in use), this is the example destined for `docs/agents/lifecycle.md` § Reusable capabilities, in that page's install style:

```ts
import { DurableObject } from "cloudflare:workers";
import { Lifecycle } from "agents/lifecycle";
import { BrowserSessions } from "agents/browser"; // once exported

export class ResearchObject extends DurableObject<Env> {
readonly browser = new BrowserSessions({
browser: this.env.BROWSER,
create: { guardrails: { allowedDomains: ["docs.example.com"] } }
});
readonly lifecycle = Lifecycle.install(this).use(this.browser);

async onRequest(_request: Request): Promise<Response> {
// First call launches the browser; later calls reattach to it.
const { cdp, restarted } = await this.browser.connect("research");
if (restarted) {
// Fresh browser — prior page state is gone; re-navigate before use.
}
try {
await cdp.send("Page.navigate", { url: "https://docs.example.com" });
Comment thread
ben-reitz marked this conversation as resolved.
} finally {
cdp.close(); // detaches the socket; the named browser stays alive
}
return new Response("done");
}
}
```

On an `Agent` subclass the only difference is installation — the Agent already owns a lifecycle, so the constructor runs `this.lifecycle.use(this.browser)` instead of `Lifecycle.install(this)`. The store needs no wiring in either style.

## Key decisions

- **Raw CDP remains the model interaction surface.** A typed verb API was prototyped and parked until evaluations can show it beats raw CDP (see the `park/browser-interaction-contract` branch).
- **Session identity is host-named, not model-promoted.** The connector's `dynamic` mode let the model promote a session; here the host wires a name and the core owns attachment. Recreation is loud (`restarted`), never silent.
- **Browser Run owns idle reclamation.** An earlier version swept idle sessions from a Lifecycle job. It duplicated `keep_alive`, woke idle objects, needed its own crash-recovery and race handling, and could delete a browser a human was driving through Live View, since that traffic bypasses the host. Dead-browser detection in `resolve` already covers correctness.
- **The connector is untouched.** Its `reuse`/`dynamic` modes overlap the session core for now; the overlap is bounded and resolves when the model surface moves onto the core.

## Tradeoffs

- Temporary duplication between connector session modes and the named session core.
- Without a sweep, records of expired browsers stay in storage until the name is next resolved or closed, and `sessions()` status is inferred from timestamps rather than asked of the platform. Browser Run can list active sessions if exact status is ever needed.
- A host that wants idle browsers reclaimed sooner than 10 minutes lowers `keepAliveMs` rather than relying on a sweep.
- Retired markers (`browser:retired:<name>`) are never deleted, so storage grows with the number of distinct names a host has ever used. Names are host-chosen and few in practice; per-user or per-task names would need an explicit forget operation.

## Relationship to browser-tools.md

[browser-tools.md](./browser-tools.md) describes the shipping codemode connector surface (`browser_execute`, quick actions, Live View, recording). This document describes the internal session layer being built beneath it. The two share `browser-run.ts`, the session stores, and `live-view.ts`.
2 changes: 2 additions & 0 deletions design/browser-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

**Status:** experimental (`agents/browser`)

> A named browser session layer (reattach-or-create sessions, restart signaling, a `BrowserSessions` lifecycle capability) is being built beneath this connector — see [browser-sessions.md](./browser-sessions.md). It is internal and not yet exported. Everything below is the shipping surface.

## Problem

Agents need full Chrome DevTools Protocol access — navigation, DOM reads, screenshots, network inspection — without shipping a generated protocol bundle, without handing LLM-generated code a raw network capability, and with browser sessions that survive the pauses a durable agent naturally takes (approvals, hibernation, long waits).
Expand Down
18 changes: 18 additions & 0 deletions docs/agents/browse-the-web.md
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,24 @@ const connector = new BrowserConnector(this.ctx, {
});
```

You can also manage Browser Rendering sessions directly. `createBrowserSession` accepts [hostname guardrails](https://developers.cloudflare.com/browser-run/features/guardrails/) that restrict which domains the session may reach — fixed at launch for every connection to the session, including Live View (not supported with Kitesurf):

```ts
import { createBrowserSession, connectBrowserSession } from "agents/browser";

const { sessionId } = await createBrowserSession(this.env.BROWSER, {
guardrails: { allowedDomains: ["example.com", "*.example.com"] }
});

// Connect (and reconnect) without deleting the session on close:
const session = await connectBrowserSession(this.env.BROWSER, sessionId, {
timeoutMs: 30_000,
onClose: () => {
/* the CDP socket closed; the session itself stays alive */
}
});
Comment thread
ben-reitz marked this conversation as resolved.
```
Comment on lines +422 to +438

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Upstream docs remain unsynchronized

Repository rules require this addition in the corresponding cloudflare-docs MDX file. The PR postpones that update until after merge, requiring tracked follow-up.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.


## Local development

Recent Wrangler releases support Browser Rendering in local development. `npx wrangler dev` provisions the browser automatically, so the same `browser: env.BROWSER` setup works locally and when deployed.
Expand Down
9 changes: 7 additions & 2 deletions packages/agents/src/browser/index.ts
Comment thread
ben-reitz marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ export {
CdpSession,
connectUrl,
type CdpSendOptions,
type CdpAttachOptions
type CdpAttachOptions,
type CdpSessionOptions
} from "./cdp-session";

export {
Expand All @@ -15,9 +16,13 @@ export {
BrowserRenderingError,
type BrowserBinding,
type BrowserRecording,
type BrowserSessionGuardrails,
type BrowserSessionInfo,
type BrowserTargetInfo,
type ConnectBrowserOptions
type ConnectBrowserOptions,
type ConnectBrowserSessionOptions,
type ConnectChromiumBrowserOptions,
type ConnectKitesurfBrowserOptions
Comment thread
ben-reitz marked this conversation as resolved.
} from "./browser-run";

export {
Expand Down
Loading