Browser

Last updated Oct 22, 2026 Give your agent a browser it can return to on later turns. Browser Run keeps the remote browser open, so your agent can continue from the same page with its tabs, cookies, and login session intact while the browser session remains alive.

Use browser tools to interact with websites, inspect pages rendered with JavaScript, or capture screenshots.

Return to the same browser

A research assistant can open a dashboard, inspect a report, and return to that page on a later turn. The Browser class keeps the remote browser session open between tool calls, including its tabs, cookies, and logins. The browserTool adapters let your harness use that browser through the Chrome DevTools Protocol (CDP).

The browser runs outside your agent's Durable Object. The object saves its session identifier and reconnects after hibernation, as long as the browser session is still alive. By default, an idle browser stays alive for up to 10 minutes. This does not save a browser permanently: expiry or a crash loses its tabs, cookies, and login state.

Use it with Pi Durable

Start with a Pi Durable agent. Install the browser runtime alongside the Pi packages:

npmyarnpnpmbun

npm i agents @cloudflare/codemode @earendil-works/pi-durable @earendil-works/pi-ai @earendil-works/chord
yarn add agents @cloudflare/codemode @earendil-works/pi-durable @earendil-works/pi-ai @earendil-works/chord
pnpm add agents @cloudflare/codemode @earendil-works/pi-durable @earendil-works/pi-ai @earendil-works/chord
bun add agents @cloudflare/codemode @earendil-works/pi-durable @earendil-works/pi-ai @earendil-works/chord

This example installs a Browser and PiHarness on a plain Durable Object. Pi chooses when to call the browser tool. Lifecycle starts both components and provides their storage.

src/index.jsjs

import { DurableObject } from "cloudflare:workers";
import { createModels } from "@earendil-works/pi-ai/models";
import { createRegistry, Harness } from "@earendil-works/pi-durable";
import { Browser, browserRun } from "agents/browser";
import { browserTool } from "agents/browser/pi";
import { PiHarness } from "agents/harness/pi";
import { Lifecycle } from "agents/lifecycle";
import { createAI } from "agents/models/pi-ai";

export { CodemodeRuntime } from "agents/browser";

export class BrowserAssistant extends DurableObject {
    ai = createAI({ binding: this.env.AI });
    browser = new Browser({ provider: browserRun(this.env.BROWSER) });
    registry = createRegistry();

harness = new PiHarness({
        harness: ({ storage, context, onWake }) => {
            const models = createModels();
            models.setProvider(this.ai.provider);
            this.registry.install({
                name: "browser",
                tools: [
                    browserTool({
                        ctx: this.ctx,
                        browser: this.browser,
                        loader: this.env.LOADER,
                    }),
                ],
            });
            return Harness.open(
                storage,
                { models, registry: this.registry, onWake },
                context,
            );
        },
        defaults: { model: this.ai("@cf/zai-org/glm-5.3-flash") },
    });

lifecycle = Lifecycle.install(this).use(this.browser).use(this.harness);

async ask(prompt) {
        const { text } = await this.harness.prompt(prompt);
        return text;
    }
}

export default {
    async fetch(request, env) {
        const assistant = env.BrowserAssistant.getByName("demo");
        const text = await assistant.ask(await request.text());
        return Response.json({ text });
    },
};

src/index.tsts

import { DurableObject } from "cloudflare:workers";
import { createModels } from "@earendil-works/pi-ai/models";
import { createRegistry, Harness } from "@earendil-works/pi-durable";
import { Browser, browserRun } from "agents/browser";
import { browserTool } from "agents/browser/pi";
import { PiHarness } from "agents/harness/pi";
import { Lifecycle } from "agents/lifecycle";
import { createAI } from "agents/models/pi-ai";

export { CodemodeRuntime } from "agents/browser";

export class BrowserAssistant extends DurableObject<Env> {
    ai = createAI({ binding: this.env.AI });
    browser = new Browser({ provider: browserRun(this.env.BROWSER) });
    registry = createRegistry();

harness = new PiHarness({
        harness: ({ storage, context, onWake }) => {
            const models = createModels();
            models.setProvider(this.ai.provider);
            this.registry.install({
                name: "browser",
                tools: [
                    browserTool({
                        ctx: this.ctx,
                        browser: this.browser,
                        loader: this.env.LOADER,
                    }),
                ],
            });
            return Harness.open(
                storage,
                { models, registry: this.registry, onWake },
                context,
            );
        },
        defaults: { model: this.ai("@cf/zai-org/glm-5.3-flash") },
    });

lifecycle = Lifecycle.install(this).use(this.browser).use(this.harness);

async ask(prompt: string) {
        const { text } = await this.harness.prompt(prompt);
        return text;
    }
}

export default {
    async fetch(request: Request, env: Env) {
        const assistant = env.BrowserAssistant.getByName("demo");
        const text = await assistant.ask(await request.text());
        return Response.json({ text });
    },
} satisfies ExportedHandler<Env>;

The example uses one named assistant for demonstration. In an application, choose an object name for each authenticated user or workspace.

Configure the model, browser, Worker Loader, and Durable Object bindings:

{
    "name": "persistent-browser-agent",
    "main": "src/index.ts",
    // Set this to today's date
    "compatibility_date": "2026-10-27",
    "compatibility_flags": ["nodejs_compat"],
    "ai": { "binding": "AI" },
    "browser": { "binding": "BROWSER" },
    "worker_loaders": [{ "binding": "LOADER" }],
    "durable_objects": {
        "bindings": [{ "name": "BrowserAssistant", "class_name": "BrowserAssistant" }],
    },
    "migrations": [{ "tag": "v1", "new_sqlite_classes": ["BrowserAssistant"] }],
}
name = "persistent-browser-agent"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-10-27"
compatibility_flags = [ "nodejs_compat" ]

[ai]
binding = "AI"

[browser]
binding = "BROWSER"

[[worker_loaders]]
binding = "LOADER"

[[durable_objects.bindings]]
name = "BrowserAssistant"
class_name = "BrowserAssistant"

[[migrations]]
tag = "v1"
new_sqlite_classes = [ "BrowserAssistant" ]

Generate the Env type with wrangler types. The LOADER binding runs model-written browser code in a sandbox. The CodemodeRuntime export makes that runtime available to the Worker.

To try persistence, send two prompts to the same assistant within the browser's idle timeout:

curl http://localhost:8787 --data 'Open https://example.com and tell me the page title.'
curl http://localhost:8787 --data 'Take a screenshot of the page you already opened without navigating again.'

The second turn can use the existing tab. Pi stores a returned screenshot as an image in the tool result. An image-capable model can see it, and a UI can display it from the transcript. This example's HTTP response only returns the final text; refer to Pi event streaming to build a UI.

Tabs, logins, and interruptions

Each Pi conversation gets its own active tab by default. Sessions, forks, and subagents can work on different pages, but cookies and logins remain shared within the Browser. A tab scope is not a separate browser profile. Use separate browsers when users need separate login sessions.

Within a conversation, browser calls run sequentially. The model can use sessionId: "active" in its CDP commands to return to its active tab. To share a tab deliberately, pass the same scope string to the Pi tools.

If a browser expires, closes, or crashes, the next use creates a replacement. A returning scope receives restarted: true in the result, indicating that its previous browser state is gone. The tool runs against the new browser; it does not restore the old session.

Pi does not automatically replay a browser call interrupted by eviction: repeating it could submit a form or click a button twice. The model receives an interrupted result and can decide how to continue. Stopping a conversation does not cancel a browser call already running. The call finishes or reaches timeoutMs, which defaults to 60 seconds.

Set keepAliveMs, recording, and hostname guardrails on browserRun(binding, options). From your application, call browser.liveView() to let a person inspect or control the same browser, and browser.close() to close it. The persistent browser currently supports Browser Run's Chromium engine.

Use another harness

The same Browser instance works with the AI SDK and TanStack AI adapters. Pass ctx when using a plain Durable Object, as in the Pi example.

Harness Import Usage
Pi Durable browserTool from agents/browser/pi Install in a Pi registry extension's tools array.
AI SDK browserTool from agents/browser/ai-sdk Add to the tools object passed to generateText or streamText.
TanStack AI browserTool from agents/browser/tanstack-ai Add to the tools array passed to chat.

For example, create an AI SDK tool inside your Durable Object:

import { browserTool } from "agents/browser/ai-sdk";

const tools = {
    browser: browserTool({
        ctx: this.ctx,
        browser: this.browser,
        loader: this.env.LOADER,
        scope: conversationId,
    }),
};
// Pass tools to generateText() or streamText().
import { browserTool } from "agents/browser/ai-sdk";

const tools = {
    browser: browserTool({
        ctx: this.ctx,
        browser: this.browser,
        loader: this.env.LOADER,
        scope: conversationId,
    }),
};
// Pass tools to generateText() or streamText().

Unlike Pi, the AI SDK and TanStack AI adapters share one active tab when scope is omitted. Pass your conversation identifier to give each conversation its own active tab. The AI SDK adapter exposes screenshots to your UI and sends the model a text summary. TanStack AI omits screenshots and directs the model to read page content instead.

For a custom harness, use Browser.connect({ scope }) from your tool implementation. It returns a CDP connection, the active tab identifier, and a restarted flag. Use setActiveTarget() to remember a tab and cdp.disconnect() to release the connection without closing the browser.

Live View and human takeover

Live View lets a person watch or control a running browser in real time. Use it when a step needs a human, such as a login, multi-factor authentication (MFA), a CAPTCHA, or sensitive input.

Hand off to a person

Call browser.liveView() from your application to get links into the same browser the model uses. It lists every open tab in the Browser, across all scopes. It does not start a browser: it returns undefined when no browser is open, or when the browser has closed or expired.

For example, add a method to the Durable Object that returns the browser's web pages and their Live View links. Call it from your Worker after you authenticate the user:

export class BrowserAssistant extends DurableObject {
    // Browser, harness, and lifecycle as in the Pi Durable example.

async getLiveView() {
        const view = await this.browser.liveView({ mode: "tab" });
        if (!view) {
            return [];
        }

return view.targets
            .filter((tab) => tab.pageUrl?.startsWith("http"))
            .map((tab) => ({
                title: tab.title,
                pageUrl: tab.pageUrl,
                liveViewUrl: tab.url,
            }));
    }
}