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.
- package/README.md +4 -2
- package/dist/{agent-routing-CDbpVjYq.d.ts → agent-routing-wS1TimZ_.d.ts} +64 -603
- package/dist/agent-routing.d.ts +1 -1
- package/dist/agent-tool-types.d.ts +1 -1
- package/dist/{agent-tools-C8DH8Yii.d.ts → agent-tools-Bamxxb6Y.d.ts} +2 -2
- package/dist/agent-tools.d.ts +1 -1
- package/dist/{ai-iXw0uOGj.d.ts → ai-CDn3Abax.d.ts} +30 -4
- package/dist/{ai-sdk-DRPh2rRa.js → ai-sdk-Kon40i0q.js} +2 -36
- package/dist/ai-sdk-Kon40i0q.js.map +1 -0
- package/dist/ai-sdk-hW3vntnk.d.ts +72 -0
- package/dist/browser/ai.d.ts +1 -1
- package/dist/browser/ai.js +16 -3
- package/dist/browser/ai.js.map +1 -1
- package/dist/browser/index.d.ts +64 -16
- package/dist/browser/index.js +4 -4
- package/dist/browser/tools/ai-sdk.d.ts +7 -70
- package/dist/browser/tools/ai-sdk.js +4 -4
- package/dist/browser/tools/ai-sdk.js.map +1 -1
- package/dist/browser/tools/pi.d.ts +9 -15
- package/dist/browser/tools/pi.js +6 -14
- package/dist/browser/tools/pi.js.map +1 -1
- package/dist/browser/tools/tanstack-ai.d.ts +46 -23
- package/dist/browser/tools/tanstack-ai.js +8 -4
- package/dist/browser/tools/tanstack-ai.js.map +1 -1
- package/dist/{browser-DccPw_wW.js → browser-1eUbPvC2.js} +413 -14
- package/dist/browser-1eUbPvC2.js.map +1 -0
- package/dist/{browser-DlXhSAdf.d.ts → browser-D9RNpJuT.d.ts} +254 -20
- package/dist/browser-tool-BhjCcNVE.d.ts +71 -0
- package/dist/browser-tool-lGnvEBLJ.js +497 -0
- package/dist/browser-tool-lGnvEBLJ.js.map +1 -0
- package/dist/capability-id-B3J8Nok_.js +13 -0
- package/dist/capability-id-B3J8Nok_.js.map +1 -0
- package/dist/chat/index.d.ts +2 -2
- package/dist/chat-sdk/index.d.ts +3 -3
- package/dist/chat-sdk/index.js +1 -1
- package/dist/client-4OlMIJx-.js.map +1 -1
- package/dist/{client-BnYbNT3z.js → client-DMWujQJ1.js} +68 -244
- package/dist/client-DMWujQJ1.js.map +1 -0
- package/dist/{client-C27CtRZr.d.ts → client-wn4-yqGz.d.ts} +9 -7
- package/dist/client.d.ts +1 -1
- package/dist/{connector-DAXBOIIi.js → connector-CM-uuxKM.js} +96 -8
- package/dist/connector-CM-uuxKM.js.map +1 -0
- package/dist/{connector-20DJ6p4k.d.ts → connector-DZ71EjE-.d.ts} +109 -9
- package/dist/experimental/webmcp.js +1 -1
- package/dist/{handler-stateless-Bly_nSH3.js → handler-stateless-B5hU3dxK.js} +35 -2
- package/dist/handler-stateless-B5hU3dxK.js.map +1 -0
- package/dist/{handler-stateless-DxYpJ_XF.d.ts → handler-stateless-C90kD5_R.d.ts} +3 -47
- package/dist/harness/container/index.d.ts +15 -15
- package/dist/harness/opencode/index.js.map +1 -1
- package/dist/harness/pi/index.d.ts +7 -3
- package/dist/harness/pi/index.js +46 -15
- package/dist/harness/pi/index.js.map +1 -1
- package/dist/harness/think/index.d.ts +2 -1
- package/dist/{images-xaDv2Fqe.js → images-CfuRCjwq.js} +1 -5
- package/dist/images-CfuRCjwq.js.map +1 -0
- package/dist/{index-DCxk3OSJ.d.ts → index-8k4qsX1T.d.ts} +2 -28
- package/dist/index.d.ts +14 -14
- package/dist/index.js +2 -2
- package/dist/internal-hosts-Kfp5A392.d.ts +230 -0
- package/dist/internal-kmw70xwR.js.map +1 -1
- package/dist/invoker-BC1SZ3q1.js +26 -0
- package/dist/invoker-BC1SZ3q1.js.map +1 -0
- package/dist/invoker-DoLFPJZV.d.ts +21 -0
- package/dist/mcp/client/index.d.ts +15 -1
- package/dist/mcp/client/index.js +2 -2
- package/dist/mcp/client/x402.d.ts +7 -5
- package/dist/mcp/client/x402.js +34 -1
- package/dist/mcp/client/x402.js.map +1 -1
- package/dist/mcp/index.d.ts +561 -27
- package/dist/mcp/index.js +180 -3
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/server/index.d.ts +1 -1
- package/dist/mcp/server/index.js +1 -1
- package/dist/models/ai-sdk/index.d.ts +2 -2
- package/dist/models/ai-sdk/index.js +1 -1
- package/dist/models/opencode/index.d.ts +2 -2
- package/dist/models/opencode/index.js +1 -1
- package/dist/models/pi-ai/index.d.ts +3 -28
- package/dist/models/pi-ai/index.js +31 -16
- package/dist/models/pi-ai/index.js.map +1 -1
- package/dist/protocol-BEgrJYC5.d.ts +29 -29
- package/dist/react.d.ts +2 -2
- package/dist/routing/index.d.ts +1 -1
- package/dist/serializable.d.ts +1 -1
- package/dist/{session-store-yvwq1f9x.js → session-7CDgVE0z.js} +455 -33
- package/dist/session-7CDgVE0z.js.map +1 -0
- package/dist/{session-store-_IK6dV-w.d.ts → session-store-B7Ca-u1t.d.ts} +8 -1
- package/dist/settings-Dk2il2Hf.js.map +1 -1
- package/dist/{src-DfLzdwlK.js → src-BGolXG_H.js} +18 -2
- package/dist/src-BGolXG_H.js.map +1 -0
- package/dist/sub-routing.d.ts +12 -12
- package/dist/tool-CTp72Jb_.d.ts +10 -10
- package/dist/tool-DoVberpN.d.ts +22 -22
- package/dist/tool-helpers-BodWi4gd.js +256 -0
- package/dist/tool-helpers-BodWi4gd.js.map +1 -0
- package/dist/{transport-CPh7YGiw.d.ts → transport-5moD2pW5.d.ts} +1 -43
- package/dist/types-D2naO3rA.d.ts +49 -0
- package/dist/voice/index.d.ts +1 -1
- package/dist/workflow-types.d.ts +1 -1
- package/dist/workflows.d.ts +1 -1
- package/dist/workflows.js +1 -1
- package/docs/browse-the-web.md +247 -413
- package/docs/get-current-agent.md +4 -7
- package/docs/harnesses/pi.md +28 -28
- package/docs/human-in-the-loop.md +3 -7
- package/docs/index.md +6 -6
- package/docs/mcp-client.md +4 -28
- package/docs/mcp-servers.md +137 -411
- package/docs/mcp-transports.md +30 -289
- package/docs/models-pi-ai.md +9 -11
- package/docs/models.md +1 -8
- package/docs/routing.md +0 -2
- package/docs/securing-mcp-servers.md +120 -304
- package/package.json +8 -3
- package/dist/ai-sdk-DRPh2rRa.js.map +0 -1
- package/dist/browser-DccPw_wW.js.map +0 -1
- package/dist/browser-tool-B1mhifin.js +0 -616
- package/dist/browser-tool-B1mhifin.js.map +0 -1
- package/dist/browser-tool-Cxjk5sgG.d.ts +0 -92
- package/dist/client-BnYbNT3z.js.map +0 -1
- package/dist/connector-DAXBOIIi.js.map +0 -1
- package/dist/handler-stateless-Bly_nSH3.js.map +0 -1
- package/dist/images-xaDv2Fqe.js.map +0 -1
- package/dist/invoker-CG0_p_Wq.d.ts +0 -20
- package/dist/invoker-CHMnoxIA.js +0 -57
- package/dist/invoker-CHMnoxIA.js.map +0 -1
- package/dist/session-store-yvwq1f9x.js.map +0 -1
- package/dist/src-DfLzdwlK.js.map +0 -1
- package/dist/tool-helpers-O5BDVEAY.js +0 -58
- package/dist/tool-helpers-O5BDVEAY.js.map +0 -1
package/docs/browse-the-web.md
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
# Browse the Web
|
|
1
|
+
# Browse the Web
|
|
2
2
|
|
|
3
|
-
Browser tools give your agents full
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
- **
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
###
|
|
25
|
+
### Setup
|
|
34
26
|
|
|
35
|
-
Add
|
|
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
|
-
|
|
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
|
-
```
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
154
|
-
ctx: this.ctx,
|
|
155
|
-
browser: env.BROWSER,
|
|
156
|
-
loader: env.LOADER
|
|
157
|
-
});
|
|
63
|
+
### pi
|
|
158
64
|
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
browser: this.env.
|
|
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
|
-
-
|
|
238
|
-
-
|
|
239
|
-
-
|
|
240
|
-
-
|
|
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
|
-
|
|
81
|
+
### TanStack AI
|
|
249
82
|
|
|
250
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
103
|
+
import { streamText } from "ai";
|
|
104
|
+
import { browserTool } from "agents/browser/ai-sdk";
|
|
268
105
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
117
|
+
### How the tool behaves
|
|
288
118
|
|
|
289
|
-
|
|
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
|
-
|
|
292
|
-
import { createQuickActionTools } from "agents/browser/ai";
|
|
127
|
+
### Configure the browser
|
|
293
128
|
|
|
294
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
144
|
+
### Live View and closing
|
|
318
145
|
|
|
319
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
+
### Driving the browser from your own code
|
|
348
151
|
|
|
349
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
168
|
+
### Internal hostnames
|
|
360
169
|
|
|
361
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
//
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 } =
|
|
425
|
-
// ...later, after the
|
|
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
|
|
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
|
-
|
|
268
|
+
## Lower-level helpers
|
|
438
269
|
|
|
439
|
-
|
|
270
|
+
For your own integrations, `agents/browser` also exports the pieces `Browser` is built on:
|
|
440
271
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
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
|
-
|
|
299
|
+
## Local development
|
|
466
300
|
|
|
467
|
-
|
|
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
|
-
|
|
303
|
+
## Security considerations
|
|
480
304
|
|
|
481
|
-
|
|
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
|
-
|
|
311
|
+
# Deprecated
|
|
484
312
|
|
|
485
|
-
|
|
486
|
-
import { BrowserConnector, CdpConnection, connectUrl } from "agents/browser";
|
|
313
|
+
These APIs keep working until they are removed, but will not change.
|
|
487
314
|
|
|
488
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
328
|
+
import { createBrowserTools } from "agents/browser/ai"; // or agents/browser/tanstack-ai
|
|
518
329
|
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
-
##
|
|
373
|
+
## Quick Actions (stateless browsing)
|
|
539
374
|
|
|
540
|
-
|
|
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
|
-
|
|
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
|
-
|
|
549
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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`.
|