Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
e5b57d2
feat(browser): run browserTool on a BrowserSession, without codemode
ben-reitz Oct 22, 2026
cbcf368
fix(browser): address review on browserTool without codemode
ben-reitz Oct 22, 2026
01a643a
docs(browser): move deprecated browser docs to the bottom, notice on …
ben-reitz Oct 22, 2026
074a412
docs(browser): rewrite browserTool's How It Works in the design doc
ben-reitz Oct 23, 2026
3261fcf
docs(browser): rewrite Browse the Web around browserTool, slim the de…
ben-reitz Oct 23, 2026
281de63
docs(browser): lead with pi, drop Experimental from Browse the Web
ben-reitz Oct 23, 2026
8da4113
docs(browser): no contractions in Browse the Web
ben-reitz Oct 23, 2026
d6643ea
docs(browser): say browserTool still needs @cloudflare/codemode insta…
ben-reitz Oct 26, 2026
b154302
fix(browser): refuse Live View, handoff and tunnelling CDP commands
ben-reitz Oct 26, 2026
e732a0d
fix(browser): a host-side deadline for browserTool runs
ben-reitz Oct 26, 2026
9005106
fix(browser): keep only real screenshots whole
ben-reitz Oct 26, 2026
4534f23
fix(browser): run an arrow function that ends in a semicolon and comment
ben-reitz Oct 26, 2026
4069d35
fix(browser): bound browserTool's console logs
ben-reitz Oct 26, 2026
258851c
docs(agents): browser rows in AGENTS.md mark the deprecated codemode …
ben-reitz Oct 26, 2026
d7ece90
chore(browser): the browserTool changeset says to keep codemode insta…
ben-reitz Oct 26, 2026
cf57109
fix(browser): cut a huge thrown error down like a result
ben-reitz Oct 26, 2026
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
5 changes: 5 additions & 0 deletions .changeset/browser-session.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agents": minor
---

`browserTool` now runs the model's code in its own Dynamic Worker instead of on codemode, so it needs no `ctx` or `CodemodeRuntime` export (keep `@cloudflare/codemode` installed for now) and its output has no `calls` log; `browser.session()` gives your own code the same connection to the browser. See [Persistent browser](https://github.com/cloudflare/agents/blob/main/docs/agents/browse-the-web.md#persistent-browser).
112 changes: 86 additions & 26 deletions design/browser-tools.md

Large diffs are not rendered by default.

594 changes: 170 additions & 424 deletions docs/agents/browse-the-web.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/agents/harnesses/pi.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ harness: async ({ storage, context, onWake }) => {
this.registry.install({
name: "browser",
tools: [
browserTool({ ctx: this.ctx, browser: this.browser, loader: this.env.LOADER })
browserTool({ browser: this.browser, loader: this.env.LOADER })
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
]
});
return Harness.open(
Expand All @@ -162,7 +162,7 @@ harness: async ({ storage, context, onWake }) => {
readonly lifecycle = Lifecycle.install(this).use(this.browser).use(this.harness);
```

It needs a `browser` binding, a `LOADER` Worker Loader binding, and `export { CodemodeRuntime } from "@cloudflare/codemode"` from the Worker entry. Screenshots come back as images, and each conversation works in its own tab. Refer to [Persistent browser](../browse-the-web.md#persistent-browser) for how it behaves.
It needs a `browser` binding, a `LOADER` Worker Loader binding, and `@cloudflare/codemode` installed (only until the deprecated codemode browser APIs are removed from `agents/browser`). Screenshots come back as images, and each conversation works in its own tab. Refer to [Persistent browser](../browse-the-web.md#persistent-browser) for how it behaves.

## Work with sessions

Expand Down
2 changes: 1 addition & 1 deletion docs/agents/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ Building your own harness? Sessions stores the conversation, Streams holds durab
- [Context (Experimental)](./context.md) - System-prompt blocks, frozen prompts, writable/searchable/loadable providers, and their tools
- [Workspace (Experimental)](https://github.com/cloudflare/agents/blob/main/docs/shell/index.md) - Durable virtual filesystem backed by SQLite + R2
- [Codemode (Experimental)](./codemode.md) - LLM-generated executable code for tool orchestration
- [Browse the Web (Experimental)](./browse-the-web.md) - Full CDP access for web inspection, scraping, and debugging
- [Browse the Web](./browse-the-web.md) - Full CDP access for web inspection, scraping, and debugging
- [Search the Web (Beta)](./search-the-web.md) - A `websearch` tool over Cloudflare's Web Search API for the pi harness, the AI SDK, and TanStack AI
- [Fetch the Web (Beta)](./fetch-the-web.md) - A `web_fetch` tool that reads a URL as Markdown, JSON, or text, with a host-controlled URL policy, for the pi harness, the AI SDK, TanStack AI, and Think
- TODO: [Cloudflare Sandboxes](./sandboxes.md) - Isolated environments for coding agents, ffmpeg, and heavy compute
Expand Down
13 changes: 8 additions & 5 deletions packages/agents/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ Each export maps to a public entry point that users `import` from. These are the
| `agents/vite` | `src/vite.ts` | Vite plugin — decorator transforms and the `agents:skills` import transform |
| `agents/skills` | `src/skills/index.ts` | Framework-agnostic Agent Skills engine — sources, `SkillRegistry`, runner |
| `agents/experimental/webmcp` | `src/experimental/webmcp.ts` | WebMCP adapter — bridges MCP tools to Chrome's `navigator.modelContext` |
| `agents/browser` | `src/browser/index.ts` | Browser Run helpers — CDP sessions, connector, Quick Action primitives |
| `agents/browser/ai` | `src/browser/ai.ts` | AI SDK browser tools — `createBrowserTools` (CDP) + `createQuickActionTools` |
| `agents/browser` | `src/browser/index.ts` | `Browser`, `browserRun()`, `BrowserSession`, CDP helpers, Quick Action primitives, deprecated `BrowserConnector` (needs codemode) |
| `agents/browser/ai` | `src/browser/ai.ts` | Deprecated AI SDK tools — `createBrowserTools` (codemode) + `createQuickActionTools` |
| `agents/browser/ai-sdk` | `src/browser/tools/ai-sdk.ts` | AI SDK `browserTool` for a persistent `Browser` (+ `createQuickActionTools` re-export) |
| `agents/browser/tanstack-ai` | `src/browser/tools/tanstack-ai.ts` | TanStack AI `browserTool` for a persistent `Browser` (+ legacy `createBrowserTools`) |
| `agents/browser/tanstack-ai` | `src/browser/tools/tanstack-ai.ts` | TanStack AI `browserTool` for a persistent `Browser` (+ deprecated `createBrowserTools`) |
| `agents/browser/pi` | `src/browser/tools/pi.ts` | pi-durable `browserTool` for a persistent `Browser` |
| `agents/websearch` | `src/web/search/index.ts` | Web Search API sources (`createAIWebSearch`, `createHTTPWebSearch`), `WebSearchError`, renderer |
| `agents/websearch/pi` | `src/web/search/tools/pi.ts` | pi harness `webSearchTool` |
Expand Down Expand Up @@ -186,13 +186,16 @@ src/
cdp-connection.ts # CdpConnection — one Chrome DevTools Protocol WebSocket
browser.ts # Browser — one named persistent browser (Lifecycle capability)
providers/ # browserRun() + its adapter; provider-adapter.ts is the internal seam Browser calls
connector.ts # BrowserConnector — codemode connector + session helpers
connector.ts # deprecated BrowserConnector — codemode connector + session helpers
session-store.ts # Durable session-id store + sweep
live-view.ts # Live View URL minting shared by Browser and BrowserConnector
spec.ts # CDP protocol spec loader (cdp.spec())
quick-actions.ts # Stateless Quick Action primitives (browserMarkdown, …)
ai.ts # createBrowserTools + createQuickActionTools (AI SDK)
ai.ts # deprecated createBrowserTools (codemode) + createQuickActionTools (deprecated)
session.ts # BrowserSession — one run's CDP session: active tab, handles, error hints, report
sandbox.ts # runs browserTool's code in a Dynamic Worker with `cdp` bound to a session
browser-tool.ts # harness-neutral browserTool core shared by the tools/ adapters
truncate.ts # truncateResult, copied from codemode
tool-helpers.ts # model-output + ctx helpers shared by ai.ts and the tools/ adapters
tanstack-ai.ts # legacy createBrowserTools for TanStack AI (re-exported by tools/tanstack-ai.ts)
tools/ # browserTool adapters for a persistent Browser: ai-sdk.ts, tanstack-ai.ts, pi.ts
Expand Down
32 changes: 6 additions & 26 deletions packages/agents/src/browser-tests/browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,10 @@ type StoredSessions = Record<
{ sessionId: string; createdAt: number; updatedAt: number }
>;

interface NamedRunOutput extends RunOutput {
interface NamedRunOutput {
status: "completed" | "error";
result?: unknown;
error?: string;
report: {
restarted: boolean;
newTabs: Array<{ targetId: string; url?: string }>;
Expand Down Expand Up @@ -603,7 +606,8 @@ describe("browser connector e2e", () => {
await callAgent("closeSession", ["reuse"]);
});
});
// The persistent named browser behind BrowserSessionConnector. These run
// The persistent named browser, driven through a BrowserSession and
// runBrowserCode as browserTool does. These run
// against real Chrome, so they also check what the unit-test fake assumes.
describe("persistent named browser", () => {
afterAll(async () => {
Expand Down Expand Up @@ -702,30 +706,6 @@ describe("browser connector e2e", () => {
);
});

it("keeps the active tab across a pause for approval", async () => {
const output = await runNamed(`async () => {
await cdp.send({
method: "Runtime.evaluate",
params: { expression: "document.title = 'paused'" },
sessionId: "active"
});
await gate.confirm({ label: "continue" });
const { result } = await cdp.send({
method: "Runtime.evaluate",
params: { expression: "document.title", returnByValue: true },
sessionId: "active"
});
return result.value;
}`);
expect(output.status).toBe("paused");

const resumed = (await callAgent("approveNamed", [
output.executionId
])) as RunOutput;
expect(resumed.status).toBe("completed");
expect(resumed.result).toBe("paused");
});

it("passes a raw Target.attachToTarget through to Chrome", async () => {
await runNamed(setTitle("raw-attach"));
const output = await runNamed(`async () => {
Expand Down
65 changes: 30 additions & 35 deletions packages/agents/src/browser-tests/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ import {
type ProxyToolOutput
} from "@cloudflare/codemode";
import { BrowserConnector } from "../browser/connector";
import {
BrowserSessionConnector,
type BrowserExecutionReport
} from "../browser/session-connector";
import { runBrowserCode } from "../browser/sandbox";
import type { BrowserSessionReport } from "../browser/session";
import { Browser, namedBrowserSessionKey } from "../browser/browser";
import { browserRun } from "../browser/providers/browser-run";
import {
Expand Down Expand Up @@ -99,7 +97,7 @@ export class BrowserTestAgent extends Agent<Env> {
)) as ProxyToolOutput;
}

// ── Persistent named browser (BrowserSessionConnector) ──
// ── Persistent named browser (BrowserSession + runBrowserCode) ──

#namedBrowser(): Browser {
return new Browser({
Expand All @@ -109,40 +107,37 @@ export class BrowserTestAgent extends Agent<Env> {
});
}

#namedConnector(scope?: string): BrowserSessionConnector {
return new BrowserSessionConnector(this.ctx, {
browser: this.#namedBrowser(),
scope
});
}

#namedRuntime(connector: BrowserSessionConnector): CodemodeRuntimeHandle {
return createCodemodeRuntime({
ctx: this.ctx,
executor: new DynamicWorkerExecutor({ loader: this.env.LOADER }),
connectors: [connector, new GateConnector(this.ctx, this.env)],
name: "browser-named"
});
}

/** Run code against the persistent browser; include the browser report. */
/**
* Run code against the persistent browser the way `browserTool`
* does: one session per run, the code in a Dynamic Worker. Includes the
* session's report.
*/
@callable()
async runNamed(
code: string,
scope?: string
): Promise<ProxyToolOutput & { report: BrowserExecutionReport | null }> {
const connector = this.#namedConnector(scope);
const output = await this.#execute(this.#namedRuntime(connector), code);
return {
...output,
report: connector.takeReport(output.executionId) ?? null
};
}

@callable()
async approveNamed(executionId: string) {
const connector = this.#namedConnector();
return this.#namedRuntime(connector).approve({ executionId });
): Promise<
(
| { status: "completed"; result: unknown }
| { status: "error"; error: string }
) & { logs: string[]; report: BrowserSessionReport | null }
> {
const session = this.#namedBrowser().session({ scope });
try {
const ran = await runBrowserCode({
loader: this.env.LOADER,
code,
session,
timeoutMs: 60_000
});
const report = (await session.close()) ?? null;
return "error" in ran
? { status: "error", error: ran.error, logs: ran.logs, report }
: { status: "completed", result: ran.result, logs: ran.logs, report };
} finally {
// Already closed unless the run threw; a second close does nothing.
await session.close();
}
}

/** Close the persistent browser so it doesn't outlive the suite. */
Expand Down
Loading
Loading