Repository navigation
docs(agents): record the browser session design and API additions #2294
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ben-reitz
merged 1 commit into
feat/browser-lifecycle-capability
from
docs/browser-interaction-design-record
Sep 24, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" }); | ||
| } 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`. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 */ | ||
| } | ||
| }); | ||
|
ben-reitz marked this conversation as resolved.
|
||
| ``` | ||
|
Comment on lines
+422
to
+438
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| ## 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. | ||
|
|
||
|
ben-reitz marked this conversation as resolved.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.