agents 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +4 -2
  2. package/dist/{agent-routing-CDbpVjYq.d.ts → agent-routing-wS1TimZ_.d.ts} +64 -603
  3. package/dist/agent-routing.d.ts +1 -1
  4. package/dist/agent-tool-types.d.ts +1 -1
  5. package/dist/{agent-tools-C8DH8Yii.d.ts → agent-tools-Bamxxb6Y.d.ts} +2 -2
  6. package/dist/agent-tools.d.ts +1 -1
  7. package/dist/{ai-iXw0uOGj.d.ts → ai-CDn3Abax.d.ts} +30 -4
  8. package/dist/{ai-sdk-DRPh2rRa.js → ai-sdk-Kon40i0q.js} +2 -36
  9. package/dist/ai-sdk-Kon40i0q.js.map +1 -0
  10. package/dist/ai-sdk-hW3vntnk.d.ts +72 -0
  11. package/dist/browser/ai.d.ts +1 -1
  12. package/dist/browser/ai.js +16 -3
  13. package/dist/browser/ai.js.map +1 -1
  14. package/dist/browser/index.d.ts +64 -16
  15. package/dist/browser/index.js +4 -4
  16. package/dist/browser/tools/ai-sdk.d.ts +7 -70
  17. package/dist/browser/tools/ai-sdk.js +4 -4
  18. package/dist/browser/tools/ai-sdk.js.map +1 -1
  19. package/dist/browser/tools/pi.d.ts +9 -15
  20. package/dist/browser/tools/pi.js +6 -14
  21. package/dist/browser/tools/pi.js.map +1 -1
  22. package/dist/browser/tools/tanstack-ai.d.ts +46 -23
  23. package/dist/browser/tools/tanstack-ai.js +8 -4
  24. package/dist/browser/tools/tanstack-ai.js.map +1 -1
  25. package/dist/{browser-DccPw_wW.js → browser-1eUbPvC2.js} +413 -14
  26. package/dist/browser-1eUbPvC2.js.map +1 -0
  27. package/dist/{browser-DlXhSAdf.d.ts → browser-D9RNpJuT.d.ts} +254 -20
  28. package/dist/browser-tool-BhjCcNVE.d.ts +71 -0
  29. package/dist/browser-tool-lGnvEBLJ.js +497 -0
  30. package/dist/browser-tool-lGnvEBLJ.js.map +1 -0
  31. package/dist/capability-id-B3J8Nok_.js +13 -0
  32. package/dist/capability-id-B3J8Nok_.js.map +1 -0
  33. package/dist/chat/index.d.ts +2 -2
  34. package/dist/chat-sdk/index.d.ts +3 -3
  35. package/dist/chat-sdk/index.js +1 -1
  36. package/dist/client-4OlMIJx-.js.map +1 -1
  37. package/dist/{client-BnYbNT3z.js → client-DMWujQJ1.js} +68 -244
  38. package/dist/client-DMWujQJ1.js.map +1 -0
  39. package/dist/{client-C27CtRZr.d.ts → client-wn4-yqGz.d.ts} +9 -7
  40. package/dist/client.d.ts +1 -1
  41. package/dist/{connector-DAXBOIIi.js → connector-CM-uuxKM.js} +96 -8
  42. package/dist/connector-CM-uuxKM.js.map +1 -0
  43. package/dist/{connector-20DJ6p4k.d.ts → connector-DZ71EjE-.d.ts} +109 -9
  44. package/dist/experimental/webmcp.js +1 -1
  45. package/dist/{handler-stateless-Bly_nSH3.js → handler-stateless-B5hU3dxK.js} +35 -2
  46. package/dist/handler-stateless-B5hU3dxK.js.map +1 -0
  47. package/dist/{handler-stateless-DxYpJ_XF.d.ts → handler-stateless-C90kD5_R.d.ts} +3 -47
  48. package/dist/harness/container/index.d.ts +15 -15
  49. package/dist/harness/opencode/index.js.map +1 -1
  50. package/dist/harness/pi/index.d.ts +7 -3
  51. package/dist/harness/pi/index.js +46 -15
  52. package/dist/harness/pi/index.js.map +1 -1
  53. package/dist/harness/think/index.d.ts +2 -1
  54. package/dist/{images-xaDv2Fqe.js → images-CfuRCjwq.js} +1 -5
  55. package/dist/images-CfuRCjwq.js.map +1 -0
  56. package/dist/{index-DCxk3OSJ.d.ts → index-8k4qsX1T.d.ts} +2 -28
  57. package/dist/index.d.ts +14 -14
  58. package/dist/index.js +2 -2
  59. package/dist/internal-hosts-Kfp5A392.d.ts +230 -0
  60. package/dist/internal-kmw70xwR.js.map +1 -1
  61. package/dist/invoker-BC1SZ3q1.js +26 -0
  62. package/dist/invoker-BC1SZ3q1.js.map +1 -0
  63. package/dist/invoker-DoLFPJZV.d.ts +21 -0
  64. package/dist/mcp/client/index.d.ts +15 -1
  65. package/dist/mcp/client/index.js +2 -2
  66. package/dist/mcp/client/x402.d.ts +7 -5
  67. package/dist/mcp/client/x402.js +34 -1
  68. package/dist/mcp/client/x402.js.map +1 -1
  69. package/dist/mcp/index.d.ts +561 -27
  70. package/dist/mcp/index.js +180 -3
  71. package/dist/mcp/index.js.map +1 -1
  72. package/dist/mcp/server/index.d.ts +1 -1
  73. package/dist/mcp/server/index.js +1 -1
  74. package/dist/models/ai-sdk/index.d.ts +2 -2
  75. package/dist/models/ai-sdk/index.js +1 -1
  76. package/dist/models/opencode/index.d.ts +2 -2
  77. package/dist/models/opencode/index.js +1 -1
  78. package/dist/models/pi-ai/index.d.ts +3 -28
  79. package/dist/models/pi-ai/index.js +31 -16
  80. package/dist/models/pi-ai/index.js.map +1 -1
  81. package/dist/protocol-BEgrJYC5.d.ts +29 -29
  82. package/dist/react.d.ts +2 -2
  83. package/dist/routing/index.d.ts +1 -1
  84. package/dist/serializable.d.ts +1 -1
  85. package/dist/{session-store-yvwq1f9x.js → session-7CDgVE0z.js} +455 -33
  86. package/dist/session-7CDgVE0z.js.map +1 -0
  87. package/dist/{session-store-_IK6dV-w.d.ts → session-store-B7Ca-u1t.d.ts} +8 -1
  88. package/dist/settings-Dk2il2Hf.js.map +1 -1
  89. package/dist/{src-DfLzdwlK.js → src-BGolXG_H.js} +18 -2
  90. package/dist/src-BGolXG_H.js.map +1 -0
  91. package/dist/sub-routing.d.ts +12 -12
  92. package/dist/tool-CTp72Jb_.d.ts +10 -10
  93. package/dist/tool-DoVberpN.d.ts +22 -22
  94. package/dist/tool-helpers-BodWi4gd.js +256 -0
  95. package/dist/tool-helpers-BodWi4gd.js.map +1 -0
  96. package/dist/{transport-CPh7YGiw.d.ts → transport-5moD2pW5.d.ts} +1 -43
  97. package/dist/types-D2naO3rA.d.ts +49 -0
  98. package/dist/voice/index.d.ts +1 -1
  99. package/dist/workflow-types.d.ts +1 -1
  100. package/dist/workflows.d.ts +1 -1
  101. package/dist/workflows.js +1 -1
  102. package/docs/browse-the-web.md +247 -413
  103. package/docs/get-current-agent.md +4 -7
  104. package/docs/harnesses/pi.md +28 -28
  105. package/docs/human-in-the-loop.md +3 -7
  106. package/docs/index.md +6 -6
  107. package/docs/mcp-client.md +4 -28
  108. package/docs/mcp-servers.md +137 -411
  109. package/docs/mcp-transports.md +30 -289
  110. package/docs/models-pi-ai.md +9 -11
  111. package/docs/models.md +1 -8
  112. package/docs/routing.md +0 -2
  113. package/docs/securing-mcp-servers.md +120 -304
  114. package/package.json +8 -3
  115. package/dist/ai-sdk-DRPh2rRa.js.map +0 -1
  116. package/dist/browser-DccPw_wW.js.map +0 -1
  117. package/dist/browser-tool-B1mhifin.js +0 -616
  118. package/dist/browser-tool-B1mhifin.js.map +0 -1
  119. package/dist/browser-tool-Cxjk5sgG.d.ts +0 -92
  120. package/dist/client-BnYbNT3z.js.map +0 -1
  121. package/dist/connector-DAXBOIIi.js.map +0 -1
  122. package/dist/handler-stateless-Bly_nSH3.js.map +0 -1
  123. package/dist/images-xaDv2Fqe.js.map +0 -1
  124. package/dist/invoker-CG0_p_Wq.d.ts +0 -20
  125. package/dist/invoker-CHMnoxIA.js +0 -57
  126. package/dist/invoker-CHMnoxIA.js.map +0 -1
  127. package/dist/session-store-yvwq1f9x.js.map +0 -1
  128. package/dist/src-DfLzdwlK.js.map +0 -1
  129. package/dist/tool-helpers-O5BDVEAY.js +0 -58
  130. package/dist/tool-helpers-O5BDVEAY.js.map +0 -1
@@ -1,12 +1,10 @@
1
- # Browse the Web (Experimental)
1
+ # Browse the Web
2
2
 
3
- Browser tools give your agents full access to the Chrome DevTools Protocol (CDP) through the code mode pattern. Instead of a fixed set of browser actions (click, screenshot, navigate), the LLM writes code that runs CDP commands against a live browser session — accessing all domains, commands, events, and types in the protocol.
3
+ Browser tools give your agents the full Chrome DevTools Protocol (CDP). Instead of a fixed set of actions (click, screenshot, navigate), the model writes code that sends CDP commands to a live browser, so it can use every domain, command, and event in the protocol.
4
4
 
5
- One durable tool is provided:
5
+ `browserTool` gives the model one browser that stays open between turns, so tabs, cookies, and logins carry over. There is a version for pi, TanStack AI, and the AI SDK.
6
6
 
7
- - **`browser_execute`** — run sandboxed code against a live browser via the `cdp` connector. Executions are recorded on a durable codemode runtime (abort-and-replay), so a run can pause for approval and resume with its browser session intact.
8
-
9
- > **Experimental** — this feature may have breaking changes in future releases.
7
+ > `createBrowserTools`, `createBrowserRuntime`, and Quick Actions are deprecated. They are documented under [Deprecated](#deprecated).
10
8
 
11
9
  ## When to use browser tools
12
10
 
@@ -16,23 +14,17 @@ Browser tools are useful when your agent needs to:
16
14
  - **Debug frontend issues** — network waterfalls, console errors, performance traces
17
15
  - **Scrape structured data** — extract content from rendered pages
18
16
  - **Capture screenshots or PDFs** — visual snapshots of web content
19
- - **Profile performance** — Core Web Vitals, JavaScript profiling, memory analysis
17
+ - **Work behind a login** — sign in once and keep the session across turns
20
18
 
21
19
  For simple page fetches where you do not need a full browser, `fetch()` is simpler. To let a model read pages as Markdown without a browser, use the [`web_fetch` tool](./fetch-the-web.md) from `agents/webfetch`.
22
20
 
23
- ## Installation
24
-
25
- Browser tools require the Agents SDK and `@cloudflare/codemode`:
26
-
27
- ```sh
28
- npm install agents @cloudflare/codemode ai zod
29
- ```
21
+ ## Persistent browser
30
22
 
31
- ## Quick Start
23
+ You create the browser once on your agent and give the model a tool that drives it. The model never starts or closes the browser.
32
24
 
33
- ### 1. Configure bindings
25
+ ### Setup
34
26
 
35
- Add the Browser Rendering and Worker Loader bindings to your `wrangler.jsonc`:
27
+ Add a [Browser Run](https://developers.cloudflare.com/browser-run/) binding and a Worker Loader binding to your `wrangler.jsonc`. The model's code runs in Dynamic Workers from the loader.
36
28
 
37
29
  ```jsonc
38
30
  // wrangler.jsonc
@@ -43,213 +35,55 @@ Add the Browser Rendering and Worker Loader bindings to your `wrangler.jsonc`:
43
35
  }
44
36
  ```
45
37
 
46
- ### 2. Export the runtime class
47
-
48
- The durable runtime behind the tool lives in a Durable Object facet, so your worker entry must export it (the `@cloudflare/codemode/vite` plugin does this automatically):
49
-
50
- ```ts
51
- export { CodemodeRuntime } from "agents/browser";
52
- ```
53
-
54
- ### 3. Create browser tools
55
-
56
- Browser tools must be created from inside a Durable Object (e.g. an Agent) — the runtime facet and the session store live on its `ctx`:
38
+ Install `agents` and `@cloudflare/codemode`, plus the package for your harness: the pi packages for [pi-durable](./harnesses/pi.md), `@tanstack/ai` for TanStack AI, or `ai` for the AI SDK.
57
39
 
58
- ```ts
59
- import { createBrowserTools } from "agents/browser/ai";
60
-
61
- const browserTools = createBrowserTools({
62
- ctx: this.ctx,
63
- browser: this.env.BROWSER,
64
- loader: this.env.LOADER
65
- });
40
+ ```sh
41
+ npm install agents @cloudflare/codemode
66
42
  ```
67
43
 
68
- If you need to connect to a custom CDP endpoint instead of the Browser Rendering binding, pass `cdpUrl`.
69
-
70
- ### 4. Use with streamText
44
+ `browserTool` does not use codemode, but `agents/browser` still exports the deprecated APIs that do, so your bundler needs it installed. That requirement goes away when those APIs are removed.
71
45
 
72
- Pass browser tools alongside your other tools:
46
+ Create a `Browser` on your Durable Object and install it on the object's [Lifecycle](./lifecycle.md), which stores the browser's record and keeps it across hibernation:
73
47
 
74
48
  ```ts
75
- import { streamText } from "ai";
76
-
77
- const result = streamText({
78
- model,
79
- system: "You are a helpful assistant that can inspect web pages.",
80
- messages,
81
- tools: {
82
- ...browserTools,
83
- ...otherTools
84
- }
85
- });
86
- ```
87
-
88
- When the LLM uses `browser_execute`, the `code` field is an async arrow function. Connector methods take a single object argument:
89
-
90
- ```javascript
91
- async () => {
92
- const { targetId } = await cdp.send({
93
- method: "Target.createTarget",
94
- params: { url: "https://example.com" }
95
- });
96
- const { sessionId } = await cdp.attachToTarget({ targetId });
97
- const { root } = await cdp.send({ method: "DOM.getDocument", sessionId });
98
- const { outerHTML } = await cdp.send({
99
- method: "DOM.getOuterHTML",
100
- params: { nodeId: root.nodeId },
101
- sessionId
102
- });
103
- await cdp.send({ method: "Target.closeTarget", params: { targetId } });
104
- return outerHTML;
105
- };
106
- ```
107
-
108
- To discover protocol surface, the model calls `cdp.spec()` — the live, normalized CDP protocol description (domains with commands, events, and types, including each command's parameters and return values) — or uses the runtime's built-in `codemode.search` / `codemode.describe`.
109
-
110
- ## Use with an Agent
111
-
112
- The typical pattern is to create browser tools inside the agent's message handler:
49
+ import { DurableObject } from "cloudflare:workers";
50
+ import { Lifecycle } from "agents/lifecycle";
51
+ import { Browser, browserRun } from "agents/browser";
113
52
 
114
- ```ts
115
- import { Agent } from "agents";
116
- import { createBrowserTools } from "agents/browser/ai";
117
- import { streamText, convertToModelMessages, stepCountIs } from "ai";
118
-
119
- export class MyAgent extends Agent<Env> {
120
- async onChatMessage() {
121
- const browserTools = createBrowserTools({
122
- ctx: this.ctx,
123
- browser: this.env.BROWSER,
124
- loader: this.env.LOADER
125
- });
126
-
127
- const result = streamText({
128
- model,
129
- system: "You can browse the web and inspect pages.",
130
- messages: await convertToModelMessages(this.messages),
131
- tools: {
132
- ...browserTools,
133
- ...this.mcp.getAITools()
134
- },
135
- stopWhen: stepCountIs(10)
136
- });
137
-
138
- return result.toUIMessageStreamResponse();
139
- }
53
+ export class MyObject extends DurableObject<Env> {
54
+ browser = new Browser({ provider: browserRun(this.env.BROWSER) });
55
+ lifecycle = Lifecycle.install(this).use(this.browser);
140
56
  }
141
57
  ```
142
58
 
143
- > Using `@cloudflare/think`? The unified execute tool (`createExecuteTool(this)`) already includes `cdp.*` alongside `state.*` and `tools.*` when `env.BROWSER` is bound. See the [Think tools documentation](https://github.com/cloudflare/agents/blob/main/docs/think/tools.md).
144
-
145
- ## TanStack AI
59
+ If you use the `Agent` class, it installs its own `Lifecycle`, so call `this.lifecycle.use(this.browser)` in its constructor instead.
146
60
 
147
- For TanStack AI, use the `/tanstack-ai` export:
148
-
149
- ```ts
150
- import { createBrowserTools } from "agents/browser/tanstack-ai";
151
- import { chat } from "@tanstack/ai";
61
+ Only Chromium on Browser Run is supported.
152
62
 
153
- const browserTools = createBrowserTools({
154
- ctx: this.ctx,
155
- browser: env.BROWSER,
156
- loader: env.LOADER
157
- });
63
+ ### pi
158
64
 
159
- const stream = chat({
160
- adapter: openaiText("gpt-4o"),
161
- tools: [...browserTools, ...otherTools],
162
- messages
163
- });
164
- ```
165
-
166
- ## Session lifecycle
167
-
168
- By default each execution gets a fresh browser session, torn down when the run ends (`one-shot`). Two more modes via the `session` option:
65
+ Import `browserTool` from `agents/browser/pi` and install it in an extension. It takes an optional `name` (default `"browser"`):
169
66
 
170
67
  ```ts
171
- createBrowserTools({
172
- ctx: this.ctx,
173
- browser: this.env.BROWSER,
174
- loader: this.env.LOADER,
175
- session: { mode: "dynamic" } // or { mode: "reuse", key: "main" }
176
- });
177
- ```
178
-
179
- - **`one-shot`** (default) — fresh session per execution; deterministic cleanup when the execution reaches a terminal status.
180
- - **`reuse`** — a named shared session that persists across executions until explicitly closed or swept.
181
- - **`dynamic`** — starts one-shot; the model can promote the session with `cdp.startSession()` (e.g. after logging in to a page) so later executions continue in the same browser.
182
-
183
- In `reuse`/`dynamic` modes the sandbox additionally gets `cdp.startSession()`, `cdp.sessionInfo()`, `cdp.closeSession()`, and `cdp.resetSession()`.
184
-
185
- Sessions are tracked durably (in the DO's storage), so they survive hibernation and approval pauses — a run that pauses for human approval resumes with its browser session, tabs, and cookies intact. If Browser Rendering expires the session while a pause waits, the resume surfaces a clear error and the model starts over.
186
-
187
- ### Host-side management and cleanup
188
-
189
- `createBrowserRuntime` returns the moving parts for host-side wiring:
190
-
191
- ```ts
192
- import { createBrowserRuntime } from "agents/browser/ai";
68
+ import { browserTool } from "agents/browser/pi";
193
69
 
194
- const { runtime, connector, tools } = createBrowserRuntime({
195
- ctx: this.ctx,
196
- browser: this.env.BROWSER,
197
- loader: this.env.LOADER,
198
- session: { mode: "dynamic" }
70
+ this.registry.install({
71
+ name: "browser",
72
+ tools: [browserTool({ browser: this.browser, loader: this.env.LOADER })]
199
73
  });
200
-
201
- await connector.sessionInfo(); // shared session id + open targets
202
- await connector.liveView(); // Live View URLs for the shared session's tabs
203
- await connector.closeSession(); // close the shared session
204
- await connector.sweep(); // reclaim expired/stale sessions — call from a scheduled task
205
- await runtime.expirePaused(); // reject stale never-approved pauses, freeing their sessions
206
- ```
207
-
208
- ## Persistent browser
209
-
210
- `browserTool` gives the model one browser that stays open between turns, so tabs, cookies, and logins carry over. You create the browser once on your agent and pass it in; the model never starts or closes it. This example uses a [Think](../think/index.md) agent, whose `getTools()` runs every turn:
211
-
212
- ```ts
213
- import { Think } from "@cloudflare/think";
214
- import { Browser, browserRun } from "agents/browser";
215
- import { browserTool } from "agents/browser/ai-sdk";
216
-
217
- export class MyAgent extends Think<Env> {
218
- browser = new Browser({ provider: browserRun(this.env.BROWSER) });
219
-
220
- constructor(ctx: DurableObjectState, env: Env) {
221
- super(ctx, env);
222
- this.lifecycle.use(this.browser);
223
- }
224
-
225
- getModel() {
226
- return "@cf/moonshotai/kimi-k2.7-code";
227
- }
228
-
229
- getTools() {
230
- return {
231
- browser: browserTool({ browser: this.browser, loader: this.env.LOADER })
232
- };
233
- }
234
- }
235
74
  ```
236
75
 
237
- - The model writes CDP code as with `createBrowserTools`. `sessionId: "active"` points at the tab it's working in, and that tab is remembered across turns.
238
- - If several chats or users share one `Browser` (for example, many threads on one agent), pass each one's id as `scope`: `browserTool({ browser, loader, scope: threadId })`. Each scope keeps its own active tab, so one chat never drives another's page, while cookies and logins stay shared. Tools that leave `scope` out share one tab. Runs in the same scope wait for each other; runs in different scopes run in parallel.
239
- - When another chat closes the model's tab, or works in it too, the result's `notice` says so.
240
- - Tabs the page opens itself come back as `newTabs` in the result.
241
- - If the browser was lost (idle past `keepAliveMs`, closed, or crashed), the code still runs in a new browser and the result includes `restarted: true`.
242
- - Call `this.browser.liveView()` to give a person a Live View link into the same browser, and `this.browser.close()` to shut it down.
243
- - Set `keepAliveMs`, `recording`, and `guardrails` on `browserRun(binding, options)`. `keepAliveMs` is how long an idle browser stays alive: 10 minutes by default, up to 20. Browser Run bills the idle time, so a longer value keeps tabs and logins across longer gaps between turns but costs more when a conversation is abandoned. Use a different `name` for each extra browser on the same agent.
244
- - Each run times out after `timeoutMs` (default 60 seconds).
245
- - The tool's description covers the `cdp` API, the CDP mistakes models commonly make, and this time limit, so the model doesn't need a discovery pass first.
246
- - A single `cdp.send` result can be at most 1 MB, so a full-page screenshot of a long page fails. The model is told to capture the viewport or use JPEG instead.
76
+ - A returned screenshot comes back as an image in the tool result, so the model can see the page and a UI can show it from the transcript. pi-ai only sends the image to models that accept images; other models get the text alone.
77
+ - Each pi conversation gets its own active tab: `scope` defaults to the calling conversation, so sessions, forks, and subagents on one object do not drive each other's page. Pass `scope` as a string to share one tab, or as a function of the call (`(api) => string`) to choose.
78
+ - pi does not rerun a browser call cut off by an eviction, because the code may have clicked or submitted something. The model gets an interrupted result and can try again; the browser itself is still there.
79
+ - Stopping a conversation does not stop a browser call already running. It finishes or times out.
247
80
 
248
- Only Chromium on a Browser Run binding is supported. It needs the same `LOADER` binding and `CodemodeRuntime` export as `createBrowserTools`.
81
+ ### TanStack AI
249
82
 
250
- For TanStack AI, import `browserTool` from `agents/browser/tanstack-ai` instead. It takes the same options plus an optional `name` (default `"browser"`) and returns a `ServerTool`:
83
+ Import `browserTool` from `agents/browser/tanstack-ai`. It takes an optional `name` (default `"browser"`) and returns a `ServerTool`:
251
84
 
252
85
  ```ts
86
+ import { chat } from "@tanstack/ai";
253
87
  import { browserTool } from "agents/browser/tanstack-ai";
254
88
 
255
89
  const stream = chat({
@@ -259,170 +93,167 @@ const stream = chat({
259
93
  });
260
94
  ```
261
95
 
262
- The TanStack AI tool doesn't support screenshots yet. TanStack AI gives the host and the model the same output, so if the model returns a screenshot, the tool replaces it with a note saying it was left out, and the tool's instructions tell the model to read the page with `Runtime.evaluate` instead.
96
+ The TanStack AI tool does not support screenshots yet. TanStack AI gives the host and the model the same output, so a returned screenshot is replaced with a note, and the tool tells the model to read the page with `Runtime.evaluate` instead.
263
97
 
264
- For [pi-durable](./harnesses/pi.md), import `browserTool` from `agents/browser/pi` and install it in an extension. It takes the same options plus an optional `name` (default `"browser"`). Outside an `Agent`, pass `ctx`:
98
+ ### AI SDK
99
+
100
+ Import `browserTool` from `agents/browser/ai-sdk` and pass it wherever you pass AI SDK tools, under any name:
265
101
 
266
102
  ```ts
267
- import { browserTool } from "agents/browser/pi";
103
+ import { streamText } from "ai";
104
+ import { browserTool } from "agents/browser/ai-sdk";
268
105
 
269
- this.registry.install({
270
- name: "browser",
271
- tools: [
272
- browserTool({
273
- ctx: this.ctx,
274
- browser: this.browser,
275
- loader: this.env.LOADER
276
- })
277
- ]
106
+ const result = streamText({
107
+ model,
108
+ messages,
109
+ tools: {
110
+ browser: browserTool({ browser: this.browser, loader: this.env.LOADER })
111
+ }
278
112
  });
279
113
  ```
280
114
 
281
- - A returned screenshot comes back as an image in the tool result, so the model can see the page and a UI can show it from the transcript. pi-ai only sends the image to models that accept images; other models get the text alone.
282
- - Each pi conversation gets its own active tab: `scope` defaults to the calling conversation, so sessions, forks, and subagents on one object don't drive each other's page. Cookies and logins are shared. Pass `scope` as a string to share one tab, or as a function of the call (`(api) => string`) to choose.
283
- - A conversation's calls run one at a time, since they share its tab.
284
- - pi doesn't rerun a browser call cut off by an eviction, because the code may have clicked or submitted something. The model gets an interrupted result and can try again; the browser itself is still there.
285
- - Stopping a conversation doesn't stop a browser call already running. It finishes or times out (`timeoutMs`, default 60 seconds).
115
+ To show a screenshot, the model returns it from its code. The full result, image included, stays on the message for your UI, and the model gets a short text summary.
286
116
 
287
- ## Quick Actions (stateless browsing)
117
+ ### How the tool behaves
288
118
 
289
- `browser_execute` drives a full, stateful CDP session — the right tool for interactive, multi-step automation. But a lot of agent browsing is really one-shot: _read this page as Markdown_, _extract these fields_, _list the links_. For those, [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/) are simpler, faster, and cheaper. They need only the `browser` binding — no Durable Object, Worker Loader, or sandbox — so they work from any Worker.
119
+ - The model writes an async arrow function against a `browser` global: `browser.cdp({ method, params, sessionId })` sends a command, `browser.attachToTarget({ targetId })` switches tabs, `browser.spec()` lists the protocol's domains and `browser.spec({ domain })` returns one, and `browser.getDebugLog()` shows recent traffic. `sessionId: "active"` points at the tab it is working in, and that tab is remembered across turns.
120
+ - The code runs in a fresh Dynamic Worker for each call, with no network access: `browser` is its only way out.
121
+ - The tool's description covers the `browser` API and the CDP mistakes models commonly make, so the model does not need a discovery pass first.
122
+ - If several chats or users share one `Browser` (for example, many threads on one Durable Object), pass each one's id as `scope`: `browserTool({ browser, loader, scope: threadId })`. Each scope keeps its own active tab, so one chat never drives another's page, while cookies and logins stay shared. Tools that leave `scope` out share one tab. Calls in the same scope wait for each other; calls in different scopes run in parallel.
123
+ - The result's `notice` tells the model what happened to its browser: it was lost (idle past `keepAliveMs`, closed, or crashed) and the code ran in a new one, another chat closed its tab or works in it too, or the page opened new tabs.
124
+ - Each call times out after `timeoutMs` (default 60 seconds).
125
+ - Results are cut to about 24,000 characters. A screenshot can be at most 1 MB, so a full-page screenshot of a long page fails; the model is told to capture the viewport or use JPEG instead.
290
126
 
291
- ```ts
292
- import { createQuickActionTools } from "agents/browser/ai";
127
+ ### Configure the browser
293
128
 
294
- const tools = createQuickActionTools({ browser: this.env.BROWSER });
295
- // browser_markdown, browser_extract, browser_links, browser_scrape
296
- const result = await generateText({ model, tools, messages });
297
- ```
298
-
299
- By default you get the text-returning, model-friendly tools; `browser_content` (raw HTML) is opt-in via `actions`.
129
+ Pass options to `browserRun(binding, options)`:
300
130
 
301
- **Context safety.** Every result is bounded to roughly `maxChars` (default 50000) so a single browse cannot blow the context window, while preserving each result's shape so the model sees a consistent type: text (markdown/content) is truncated to a string, oversized link/scrape arrays are trimmed but stay arrays, and only an opaque oversized object (e.g. a sprawling `extract`) degrades to a `{ truncated, note, preview }` summary. Set `maxChars: 0` to disable.
131
+ - `keepAliveMs` is how long an idle browser stays alive: 10 minutes by default, up to 20. Browser Run bills the idle time, so a longer value keeps tabs and logins across longer gaps between turns but costs more when a conversation is abandoned.
132
+ - `guardrails` restricts which hostnames the browser may reach, for example `{ allowedDomains: ["example.com", "*.example.com"] }`. See [guardrails](https://developers.cloudflare.com/browser-run/features/guardrails/).
133
+ - `recording` turns on [session recording](#session-recording).
302
134
 
303
- **Authenticated and JavaScript-heavy pages.** The model only ever supplies the page (`url`/`html`) and action-specific fields. Host-supplied options — `cookies`, `authenticate`, `setExtraHTTPHeaders` for protected pages, or `gotoOptions` / `viewport` for pages that need to settle — are passed once via `options` and merged into every request:
135
+ To have more than one browser on a Durable Object, give each one a different `name` when calling `new Browser()`:
304
136
 
305
137
  ```ts
306
- const tools = createQuickActionTools({
307
- browser: this.env.BROWSER,
308
- actions: ["markdown", "extract", "links"],
309
- maxChars: 20_000,
310
- options: {
311
- authenticate: { username: "user", password: env.SITE_PASSWORD },
312
- gotoOptions: { waitUntil: "networkidle0" }
313
- }
138
+ research = new Browser({
139
+ name: "research",
140
+ provider: browserRun(this.env.BROWSER)
314
141
  });
315
142
  ```
316
143
 
317
- You can also call the primitives directly:
144
+ ### Live View and closing
318
145
 
319
- ```ts
320
- import { browserMarkdown, browserExtract } from "agents/browser";
321
-
322
- const md = await browserMarkdown(this.env.BROWSER, { url });
323
- const data = await browserExtract<{ price: number }>(this.env.BROWSER, {
324
- url,
325
- prompt: "the product price",
326
- response_format: { type: "json_schema", json_schema: priceSchema }
327
- });
328
- ```
329
-
330
- For an endpoint or option not yet wrapped, `runQuickAction(browser, action, params)` returns the raw `Response`; its `params` are typed against the action (`"json"` expects an extract input, `"scrape"` expects `elements`, and so on).
331
-
332
- **Using them with `browser_execute`.** Quick Actions and the durable CDP tool share the same `BROWSER` binding and complement each other — one-shot reads versus interactive sessions. `createBrowserTools` / `createBrowserRuntime` expose **both by default** whenever a `browser` binding is present, and resolve `ctx` from the current Agent (via `getCurrentAgent()`) so you can skip threading `this.ctx`:
146
+ Call `this.browser.liveView()` to get [Live View](https://developers.cloudflare.com/browser-run/features/live-view/) links into the browser's open tabs, so a person can watch or take over, for example to log in. The links are credentials that can only be opened for about five minutes: give them to a trusted person, never to the model. The agent can end its turn and pick up once the person is done.
333
147
 
334
- ```ts
335
- // Inside an Agent method — ctx is picked up automatically:
336
- const tools = createBrowserTools({
337
- browser: this.env.BROWSER,
338
- loader: this.env.LOADER
339
- });
340
- // browser_execute + browser_markdown + browser_extract + browser_links + browser_scrape
341
-
342
- // Configure or disable the Quick Action half:
343
- createBrowserTools({ browser, loader, quickActions: { maxChars: 20_000 } });
344
- createBrowserTools({ browser, loader, quickActions: false });
345
- ```
148
+ Call `this.browser.close()` to shut the browser down. The next call starts a new one.
346
149
 
347
- When only `cdpUrl` is set (no binding), the Quick Action tools are skipped silently.
150
+ ### Driving the browser from your own code
348
151
 
349
- **Using them with `@cloudflare/think`.** Quick Action tools are an ordinary `ToolSet`, so a Think agent exposes them by spreading them from `getTools()`:
152
+ `browser.session()` gives you what the tool's `browser` global is built on: one connection to the browser, with the same `sessionId: "active"` handling and error hints. Close it when you are done to save the scope's active tab and get a report of what happened to the browser:
350
153
 
351
154
  ```ts
352
- class Researcher extends Think<Env> {
353
- getTools() {
354
- return { ...createQuickActionTools({ browser: this.env.BROWSER }) };
355
- }
155
+ const session = this.browser.session({ scope: threadId });
156
+ try {
157
+ await session.send({
158
+ method: "Page.navigate",
159
+ params: { url: "https://example.com" },
160
+ sessionId: "active"
161
+ });
162
+ } finally {
163
+ const report = await session.close();
164
+ // { restarted, newTabs, tabClosed, tabShared }, or undefined if unused
356
165
  }
357
166
  ```
358
167
 
359
- Quick Actions require a Worker `compatibility_date` of `2026-03-24` or later and `remote: true` on the browser binding for local `wrangler dev`.
168
+ ### Internal hostnames
360
169
 
361
- ## Using Kitesurf
362
-
363
- To use Kitesurf with the CDP-based `browser_execute` tool, select it in the session options:
170
+ Give the browser hostnames that your Durable Object serves instead of the internet: files in an R2 bucket, a folder in its workspace, a dev server in its sandbox, or another Worker. Each host has a `fetch` function that runs in your Durable Object, so it can use its bindings and state:
364
171
 
365
172
  ```ts
366
- const tools = createBrowserTools({
367
- ctx: this.ctx,
368
- browser: this.env.BROWSER,
369
- loader: this.env.LOADER,
370
- session: { browser: "kitesurf" }
371
- });
372
- ```
373
-
374
- A Kitesurf browser is scoped to its CDP WebSocket. It therefore supports only one-shot execution. Session reuse, pause and resume, Live View, recording, `keepAliveMs`, protocol discovery, and Kitesurf-backed Quick Actions are not available.
375
-
376
- ## Live View and human-in-the-loop
377
-
378
- [Live View](https://developers.cloudflare.com/browser-run/features/live-view/) lets a human open a URL and watch — or take control of — a running browser session in real time. It is the building block for human-in-the-loop steps such as logging in, solving a CAPTCHA, completing MFA, or entering data you do not want to pass through an automation script.
379
-
380
- Because the codemode runtime can already pause a run for approval _with the browser session intact_, a handoff is a four-step pattern:
381
-
382
- 1. The model calls `cdp.getLiveViewUrl()` to get a link to the current tab.
383
- 2. It surfaces the link to the user (for example by returning it, writing it to state, or sending it via Slack or email).
384
- 3. It makes an approval-gated call, so the run pauses durably while the human acts in the live browser.
385
- 4. After approval, the run resumes against the same session — cookies and login state intact.
386
-
387
- ```javascript
388
- async () => {
389
- const { targetId } = await cdp.send({
390
- method: "Target.createTarget",
391
- params: { url: "https://example.com/login" }
173
+ import { DurableObject } from "cloudflare:workers";
174
+ import { Lifecycle } from "agents/lifecycle";
175
+ import { Browser, browserRun, type InternalHostMessage } from "agents/browser";
176
+ import { z } from "zod";
177
+ // Your own code: serve a workspace folder's files, and reach a port on your
178
+ // sandbox. Each returns a Response.
179
+ import { fetchSandboxPort, serveWorkspaceFolder } from "./hosts";
180
+
181
+ // Browser Run calls back into your Worker through this entrypoint.
182
+ export { InternalHostRouter } from "agents/browser";
183
+
184
+ export class MyObject extends DurableObject<Env> {
185
+ browser = new Browser({
186
+ provider: browserRun(this.env.BROWSER, {
187
+ guardrails: { allowedDomains: [] }, // internal hosts only
188
+ internalHosts: {
189
+ router: this.ctx.exports.InternalHostRouter,
190
+ hosts: {
191
+ "assets.internal": {
192
+ description: "Your app's static assets, served from an R2 bucket",
193
+ fetch: async (request) => {
194
+ const { pathname } = new URL(request.url);
195
+ const file = await this.env.ASSETS_BUCKET.get(
196
+ decodeURIComponent(pathname.slice(1))
197
+ );
198
+ if (!file) return new Response("Not found", { status: 404 });
199
+ const headers = new Headers();
200
+ file.writeHttpMetadata(headers); // its Content-Type
201
+ return new Response(file.body, { headers });
202
+ }
203
+ },
204
+ "site.internal": {
205
+ description: "A workspace folder, such as a production build",
206
+ target: z.object({
207
+ folder: z
208
+ .string()
209
+ .startsWith("/workspace/")
210
+ .refine((path) => !path.split("/").includes(".."))
211
+ }),
212
+ fetch: (request, { folder }) =>
213
+ serveWorkspaceFolder(folder, request)
214
+ },
215
+ "preview.internal": {
216
+ description: "Your dev servers",
217
+ target: z.object({ port: z.number().int().min(1024) }),
218
+ fetch: (request, { port }) => fetchSandboxPort(port, request)
219
+ },
220
+ "api.internal": { worker: this.env.API }
221
+ }
222
+ }
223
+ })
392
224
  });
225
+ lifecycle = Lifecycle.install(this).use(this.browser);
393
226
 
394
- // A link the user opens to log in themselves.
395
- const { url } = await cdp.getLiveViewUrl({ targetId, mode: "tab" });
396
- return { needsHumanLogin: url };
397
- };
227
+ // InternalHostRouter calls this to reach your Browser.
228
+ fetchInternalHost(message: InternalHostMessage) {
229
+ return this.browser.fetchInternalHost(message);
230
+ }
231
+ }
398
232
  ```
399
233
 
400
- Pass `mode: "tab"` for a standalone interactive page view (best for a handoff) or `mode: "devtools"` for the full DevTools inspector panel. The URL is valid for about five minutes; call again for a fresh one.
234
+ - A host with `fetch` always serves the same thing. Add a `target` schema and the model chooses: it calls `browser.assignHost({ host, target })` in its browser code, the target is checked against the schema and stored, and your `fetch` gets it as its second argument. `target` takes a [Standard JSON Schema](https://standardschema.dev) (Zod 4.2+, ArkType, Valibot via its adapter), which types the argument, or a plain JSON Schema object, which leaves it `unknown`.
235
+ - Assignments survive the browser being replaced, eviction, and redeploys. An assignable host answers 502 until it is assigned. Call `this.browser.internalHosts.assign()`, `.clear()`, or `.list()` to manage them yourself.
236
+ - A host with `worker` (a service binding or a `ctx.exports` entrypoint) goes straight to that Worker, without passing through your Durable Object.
237
+ - The browser tool's description lists each host with its description and schema, and its `browser` global gains `listHosts()`, plus `assignHost()` and `clearHost()` when a host is assignable.
238
+ - Internal hosts are plain http on port 80. There is no https and no WebSockets, so a dev server's hot reload will not connect.
239
+ - At most 8 hostnames, as bare lowercase names ending in `.internal`, `.test`, `.example`, or `.invalid` (`preview.internal`, not `http://preview.internal:5173`). These suffixes never resolve on the internet, so a host cannot shadow a real site. They are fixed when the browser starts. Changing the set replaces the browser on its next use; changing a `fetch` function does not. Pointing a `worker` host at a different Worker does not either, so call `this.browser.close()` after such a change.
240
+ - `guardrails` do not apply to internal hosts. `allowedDomains: []` lets the browser reach only them.
241
+ - Any plain http page open in the browser can send requests to internal hosts, including a page on another internal host (Chrome blocks this from https pages as mixed content). Use `allowedDomains: []`, and if a host's requests have side effects, check that their `Origin` is that host.
242
+ - When a host's `fetch` throws, the browser gets a 502 and the error is logged. The page does not see the error message.
243
+ - Internal hosts only work when deployed. In local dev the browser starts without them and logs a warning.
244
+ - Install the `Browser` on a top-level Durable Object or agent. Sub-agents are not supported yet.
401
245
 
402
- From the host side, `connector.liveView()` returns the shared (`reuse`/`dynamic`) session's tabs and their Live View URLs, so an agent can render a "take over this session" link in its own UI without entering the sandbox. Each tab also carries its current `pageUrl`, so a UI can label tabs and skip blank or internal (`about:blank`, `chrome://`) pages.
246
+ `InternalHostRouter` reaches your Durable Object by calling its `fetchInternalHost()` method. With more than one `Browser`, pick the one named by `message.browser`. Without the method, the browser shows a 502 that says what to add. `Agent` has the method built in, so on an agent leave it out.
403
247
 
404
248
  ## Session recording
405
249
 
406
- Where Live View lets you watch a session _live_, [session recording](https://developers.cloudflare.com/browser-run/features/session-recording/) captures everything the agent did so you can review it _afterward_ — an [rrweb](https://github.com/rrweb-io/rrweb) capture of DOM changes, input, and navigation (structured JSON, not video). It is the natural audit trail for an autonomous browser run.
407
-
408
- Opt in per session via the `session` option; sessions this connector creates then record until they close:
409
-
410
- ```ts
411
- const { connector, tools } = createBrowserRuntime({
412
- ctx: this.ctx,
413
- browser: this.env.BROWSER,
414
- loader: this.env.LOADER,
415
- session: { mode: "reuse", key: "main", recording: true }
416
- });
417
- ```
418
-
419
- A recording is only finalized once the session closes (an explicit `connector.closeSession()`, idle `keep_alive` expiry, or `connector.sweep()`), so capture the session id while the session is alive and fetch the recording later:
250
+ [Session recording](https://developers.cloudflare.com/browser-run/features/session-recording/) captures everything the agent did so you can review it afterward: an [rrweb](https://github.com/rrweb-io/rrweb) capture of DOM changes, input, and navigation (structured JSON, not video). Turn it on with `browserRun(binding, { recording: true })`. A recording is finalized when the browser closes, so note the session id while the browser is alive and fetch the recording later:
420
251
 
421
252
  ```ts
422
253
  import { getBrowserRecording } from "agents/browser";
423
254
 
424
- const { sessionId } = (await connector.sessionInfo()) ?? {};
425
- // ...later, after the session has closed...
255
+ const { sessionId } = await this.browser.resolve();
256
+ // ...later, after the browser has closed...
426
257
  const recording = await getBrowserRecording({
427
258
  accountId: this.env.CF_ACCOUNT_ID,
428
259
  apiToken: this.env.CF_API_TOKEN,
@@ -432,124 +263,127 @@ const recording = await getBrowserRecording({
432
263
  // ready to hand to rrweb-player.
433
264
  ```
434
265
 
435
- Retrieval goes through the Browser Rendering REST API, so it needs an account id and an API token with `Browser Rendering` read access (the Workers binding cannot read recordings). Recordings are retained for 30 days and capped at 2 hours per session.
266
+ Retrieval goes through the Browser Run REST API, so it needs an account id and an API token with `Browser Rendering` read access. Recordings are retained for 30 days and capped at 2 hours per session. A recording spans the browser's whole life, across every turn and every scope sharing it. rrweb masks input fields by default, but treat a recording as potentially sensitive.
436
267
 
437
- Be deliberate with recording on shared (`reuse`/`dynamic`) sessions: the recording spans the session's _entire_ lifetime — every turn, and every user that shares the session `key` — until it closes. rrweb masks input fields by default, but treat a recording as potentially sensitive and scope the session `key` accordingly.
268
+ ## Lower-level helpers
438
269
 
439
- ## Execution model
270
+ For your own integrations, `agents/browser` also exports the pieces `Browser` is built on:
440
271
 
441
- - LLM-generated code runs in a Worker sandbox; the CDP WebSocket and the browser session stay in the host worker.
442
- - Every `cdp.*` call is recorded in the runtime's durable log. If a run pauses (approval) or the sandbox aborts, resuming replays the log and continues — which is why connector calls must be sequential and deterministic (wrap nondeterministic non-connector work in `codemode.step`).
443
- - `cdp.attachToTarget` returns `{ sessionId }` where the id is a **stable session handle** (not a raw CDP session id), so handles stay valid across pause/resume reconnects.
444
- - The protocol spec is fetched from the live browser, normalized, and cached per binding.
445
-
446
- ## CDP connector API
272
+ ```ts
273
+ import {
274
+ connectUrl,
275
+ createBrowserSession,
276
+ connectBrowserSession,
277
+ loadCdpSpec
278
+ } from "agents/browser";
279
+
280
+ // A CDP connection to any endpoint, such as a local Chrome
281
+ const connection = await connectUrl("http://localhost:9222");
282
+ const version = await connection.send("Browser.getVersion");
283
+ connection.close();
447
284
 
448
- Inside `browser_execute`, the `cdp` namespace provides (all methods take one object argument):
285
+ // A Browser Run session you manage yourself
286
+ const { sessionId } = await createBrowserSession(this.env.BROWSER, {
287
+ guardrails: { allowedDomains: ["example.com"] }
288
+ });
289
+ const session = await connectBrowserSession(this.env.BROWSER, sessionId, {
290
+ timeoutMs: 30_000
291
+ });
449
292
 
450
- | Method | Description |
451
- | ------------------------------------------------------- | ------------------------------------------------------------------------------ |
452
- | `cdp.send({ method, params?, sessionId?, timeoutMs? })` | Send a CDP command and wait for the response |
453
- | `cdp.attachToTarget({ targetId, timeoutMs? })` | Attach to a target; returns `{ sessionId }` for page-scoped `send` calls |
454
- | `cdp.spec()` | The searchable, normalized CDP protocol spec |
455
- | `cdp.getDebugLog({ limit? })` | Recent CDP traffic (sends, receives, warnings) for this execution's connection |
456
- | `cdp.clearDebugLog()` | Clear the debug log buffer |
457
- | `cdp.getLiveViewUrl({ targetId?, mode? })` | A Live View URL a human can open to watch/control the session in real time |
458
- | `cdp.startSession()` _(reuse/dynamic)_ | Promote/ensure the shared session; returns its info |
459
- | `cdp.sessionInfo()` _(reuse/dynamic)_ | Shared session info, or `null` |
460
- | `cdp.closeSession()` _(reuse/dynamic)_ | Close the shared session |
461
- | `cdp.resetSession()` _(reuse/dynamic)_ | Close and replace the shared session |
293
+ // The normalized CDP protocol, read from a live browser
294
+ const spec = await loadCdpSpec({ browser: this.env.BROWSER, sessionId });
295
+ ```
462
296
 
463
- ## Configuration
297
+ `connectBrowserSession` does not delete the session when the connection closes, so you can reconnect. `loadCdpSpec` without a `sessionId` creates a temporary session to read the protocol, then deletes it.
464
298
 
465
- ### `createBrowserTools(options)` / `createBrowserRuntime(options)`
299
+ ## Local development
466
300
 
467
- | Option | Type | Default | Description |
468
- | ------------ | -------------------------------- | ---------- | ---------------------------------------------------------- |
469
- | `ctx` | `DurableObjectState` | required | The DO hosting the runtime facet and session store |
470
- | `browser` | `BrowserBinding` | — | Browser Rendering binding |
471
- | `cdpUrl` | `string` | — | Optional override for a custom CDP endpoint |
472
- | `cdpHeaders` | `Record<string, string>` | — | Headers for CDP URL discovery (e.g. Cloudflare Access) |
473
- | `loader` | `WorkerLoader` | required | Worker Loader binding for sandboxed execution |
474
- | `session` | `BrowserConnectorSessionOptions` | `one-shot` | Session lifecycle mode |
475
- | `store` | `BrowserSessionStore` | DO storage | Durable store for session ids |
476
- | `timeout` | `number` | `30000` | Execution timeout (also the per-CDP-command timeout) |
477
- | `name` | `string` | `browser` | Runtime name — the durable identity of executions/snippets |
301
+ Recent Wrangler releases support Browser Run in local development. `npx wrangler dev` provisions the browser automatically, so the same `browser: env.BROWSER` setup works locally and when deployed.
478
302
 
479
- Either `browser` or `cdpUrl` must be provided. When both are set, `cdpUrl` takes priority.
303
+ ## Security considerations
480
304
 
481
- ### Raw access
305
+ - The model's code runs in a fresh Dynamic Worker for every call, with no network access (`fetch` and `connect` are blocked). `browser` is its only way out.
306
+ - CDP commands go to your Durable Object over Workers RPC. The WebSocket and the browser stay in your Durable Object, not the sandbox.
307
+ - The model cannot close or crash the browser; `Browser.close` and `Browser.crash*` are refused.
308
+ - The model cannot get a Live View link: `Cloudflare.getLiveView` and `Cloudflare.handoff` are refused, and so are `Target.sendMessageToTarget` and `Target.exposeDevToolsProtocol`, which could carry those commands inside them.
309
+ - Live View links give whoever holds them control of the browser, including its logins. Do not show them to the model or store them.
482
310
 
483
- For custom integrations, import the building blocks directly:
311
+ # Deprecated
484
312
 
485
- ```ts
486
- import { BrowserConnector, CdpConnection, connectUrl } from "agents/browser";
313
+ These APIs keep working until they are removed, but will not change.
487
314
 
488
- // Connect to a custom CDP endpoint
489
- const connection = await connectUrl("http://localhost:9222");
490
- const version = await connection.send("Browser.getVersion");
491
- connection.close();
315
+ ## `createBrowserTools` and `createBrowserRuntime`
492
316
 
493
- // Or plug the connector into your own codemode runtime
494
- const connector = new BrowserConnector(this.ctx, {
495
- browser: this.env.BROWSER,
496
- store,
497
- session: { mode: "dynamic" }
498
- });
499
- ```
317
+ > **Deprecated.** Use [`browserTool`](#persistent-browser). Pausing a run with its session kept while a person works in Live View has no replacement yet, so keep using `createBrowserTools` if you rely on it.
500
318
 
501
- To load the normalized CDP protocol specification from Worker code, use `loadCdpSpec()`:
319
+ `createBrowserTools` returns a `browser_execute` tool that runs the model's code against a browser through a `cdp` connector on a durable codemode runtime. A run can pause for approval and resume with its browser session intact. It needs `@cloudflare/codemode`, the same `BROWSER` and `LOADER` bindings as `browserTool`, and a `CodemodeRuntime` export from your worker (the `@cloudflare/codemode/vite` plugin adds it):
502
320
 
503
321
  ```ts
504
- import { loadCdpSpec } from "agents/browser";
505
-
506
- const spec = await loadCdpSpec({
507
- browser: this.env.BROWSER,
508
- sessionId
509
- });
322
+ export { CodemodeRuntime } from "agents/browser";
510
323
  ```
511
324
 
512
- Pass a Browser Run binding in `browser` and the Browser Run session ID returned by `createBrowserSession()` in `sessionId`. If you omit `sessionId`, the helper creates a temporary session to fetch the specification, then deletes it. Pass an existing session ID to read the specification without creating another session. If you provide `cdpUrl`, the helper loads the specification from that URL instead; `cdpUrl` takes precedence over `browser` and `sessionId`.
513
-
514
- 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):
325
+ Create the tools inside a Durable Object, such as an Agent:
515
326
 
516
327
  ```ts
517
- import { createBrowserSession, connectBrowserSession } from "agents/browser";
328
+ import { createBrowserTools } from "agents/browser/ai"; // or agents/browser/tanstack-ai
518
329
 
519
- const { sessionId } = await createBrowserSession(this.env.BROWSER, {
520
- guardrails: { allowedDomains: ["example.com", "*.example.com"] }
521
- });
522
-
523
- // Connect (and reconnect) without deleting the session on close:
524
- const session = await connectBrowserSession(this.env.BROWSER, sessionId, {
525
- timeoutMs: 30_000,
526
- onClose: () => {
527
- /* the CDP socket closed; the session itself stays alive */
528
- }
330
+ const tools = createBrowserTools({
331
+ ctx: this.ctx,
332
+ browser: this.env.BROWSER,
333
+ loader: this.env.LOADER,
334
+ session: { mode: "dynamic" }
529
335
  });
530
336
  ```
531
337
 
532
- ## Local development
533
-
534
- 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.
535
-
536
- Use `cdpUrl` only when you intentionally want to connect to some other CDP-compatible browser endpoint, such as a tunnel or a manually managed Chrome instance.
338
+ | Option | Type | Default | Description |
339
+ | ------------ | -------------------------------- | ------------- | ---------------------------------------------------------- |
340
+ | `ctx` | `DurableObjectState` | current Agent | The DO hosting the runtime facet and session store |
341
+ | `browser` | `BrowserBinding` | — | Browser Run binding |
342
+ | `cdpUrl` | `string` | — | A custom CDP endpoint; takes priority over `browser` |
343
+ | `cdpHeaders` | `Record<string, string>` | — | Headers for CDP URL discovery (e.g. Cloudflare Access) |
344
+ | `loader` | `WorkerLoader` | required | Worker Loader binding for sandboxed execution |
345
+ | `session` | `BrowserConnectorSessionOptions` | `one-shot` | Session lifecycle mode |
346
+ | `store` | `BrowserSessionStore` | DO storage | Durable store for session ids |
347
+ | `timeout` | `number` | `30000` | Execution timeout (also the per-CDP-command timeout) |
348
+ | `name` | `string` | `browser` | Runtime name — the durable identity of executions/snippets |
349
+
350
+ Session modes:
351
+
352
+ - **`one-shot`** (default) — a fresh session per execution, closed when it ends.
353
+ - **`reuse`** — a named shared session (`{ mode: "reuse", key }`) that persists across executions until closed or swept.
354
+ - **`dynamic`** — starts one-shot; the model can keep the session with `cdp.startSession()`.
355
+
356
+ Set `session: { recording: true }` to record sessions, or `session: { browser: "kitesurf" }` to use Kitesurf (one-shot only). `createBrowserRuntime` takes the same options and also returns the `connector` for host-side calls: `sessionInfo()`, `liveView()`, `closeSession()`, and `sweep()` (call it from a scheduled task to reclaim stale sessions).
357
+
358
+ The model's `cdp` connector:
359
+
360
+ | Method | Description |
361
+ | ------------------------------------------------------------- | -------------------------------------------------------------- |
362
+ | `cdp.send({ method, params?, sessionId?, timeoutMs? })` | Send a CDP command |
363
+ | `cdp.attachToTarget({ targetId, timeoutMs? })` | Attach to a target; returns a `sessionId` that survives resume |
364
+ | `cdp.spec()` | The normalized CDP protocol |
365
+ | `cdp.getDebugLog({ limit? })` / `cdp.clearDebugLog()` | Recent CDP traffic for this execution |
366
+ | `cdp.getLiveViewUrl({ targetId?, mode? })` | A Live View URL for a person to watch or take over |
367
+ | `cdp.startSession()` _(reuse/dynamic)_ | Keep the shared session; returns its info |
368
+ | `cdp.sessionInfo()` _(reuse/dynamic)_ | Shared session info, or `null` |
369
+ | `cdp.closeSession()` / `cdp.resetSession()` _(reuse/dynamic)_ | Close, or close and replace, the shared session |
370
+
371
+ For a Live View handoff, the model calls `cdp.getLiveViewUrl()`, shows the link to the user, then makes an approval-gated call. The run pauses while the person works in the browser and resumes in the same session. Connector calls are recorded and replayed on resume, so they must be sequential (no `Promise.all`).
537
372
 
538
- ## Security considerations
373
+ ## Quick Actions (stateless browsing)
539
374
 
540
- - LLM-generated code runs in **isolated Worker sandboxes** — each execution gets its own Worker instance
541
- - External network access (`fetch`, `connect`) is **blocked** in the sandbox at the runtime level
542
- - CDP commands are dispatched via Workers RPC — the WebSocket lives in the host, not the sandbox
543
- - The CDP spec stays on the server — only query results flow to the LLM
544
- - Completed results are truncated to approximately 6,000 tokens to prevent context window overflow; the full result is kept on the execution record
375
+ > **Deprecated.** Use [`browserTool`](#persistent-browser) to drive a browser, the [`web_fetch` tool](./fetch-the-web.md) from `agents/webfetch` to read a page, or call the binding's `quickAction()` directly.
545
376
 
546
- ## Current limitations
377
+ [Quick Actions](https://developers.cloudflare.com/browser-run/quick-actions/) are one-shot Browser Run calls that need only the `browser` binding. `createQuickActionTools` turns them into tools, and `createBrowserTools` includes them by default when a `browser` binding is present (pass `quickActions: false` to leave them out):
547
378
 
548
- - **Sequential connector calls** — the durable replay log requires deterministic ordering, so model code must not `Promise.all` CDP calls (the tool instructions enforce this).
549
- - **Local development depends on Wrangler support** — if Browser Rendering local mode is unavailable in your environment, upgrade Wrangler or provide `cdpUrl` explicitly. The local simulator also differs from production in places (e.g. session DELETE is a no-op).
550
- - **No authenticated sessions out of the box** — the browser starts without cookies or login state, but with `dynamic`/`reuse` modes a logged-in session can be kept alive across executions.
551
- - Requires `@cloudflare/codemode` as a peer dependency
379
+ ```ts
380
+ import { createQuickActionTools } from "agents/browser/ai";
552
381
 
553
- ## Example
382
+ const tools = createQuickActionTools({
383
+ browser: this.env.BROWSER,
384
+ maxChars: 20_000 // default 50000; 0 turns truncation off
385
+ });
386
+ // browser_markdown, browser_extract, browser_links, browser_scrape
387
+ ```
554
388
 
555
- See [`examples/ai-chat/`](https://github.com/cloudflare/agents/tree/main/examples/ai-chat) for a working example that combines browser tools with other AI SDK tools, MCP servers, and tool approval, and [`examples/codemode-connectors/`](https://github.com/cloudflare/agents/tree/main/examples/codemode-connectors) for the connector playground (including in-sandbox approvals).
389
+ Pass `actions` to choose tools (`browser_content`, raw HTML, is opt-in) and `options` for host-supplied settings such as `cookies`, `authenticate`, or `gotoOptions`. The same calls are exported as functions: `browserMarkdown`, `browserExtract`, and `runQuickAction(browser, action, params)` for the raw `Response`. Quick Actions need a `compatibility_date` of `2026-03-24` or later, and `remote: true` on the browser binding for local `wrangler dev`.