agents 0.16.2 → 0.17.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 (122) hide show
  1. package/README.md +11 -8
  2. package/dist/{agent-tool-types-CTw3UJUP.d.ts → agent-tool-types-Cd1TZPfB.d.ts} +587 -137
  3. package/dist/agent-tool-types.d.ts +34 -18
  4. package/dist/agent-tool-types.js +20 -1
  5. package/dist/agent-tool-types.js.map +1 -0
  6. package/dist/agent-tools-BXlsuX0d.js +304 -0
  7. package/dist/agent-tools-BXlsuX0d.js.map +1 -0
  8. package/dist/agent-tools-_E8wxUIK.d.ts +119 -0
  9. package/dist/agent-tools.d.ts +24 -18
  10. package/dist/agent-tools.js.map +1 -1
  11. package/dist/ai-chat-agent.d.ts +1 -1
  12. package/dist/ai-chat-agent.js +1 -2
  13. package/dist/ai-chat-agent.js.map +1 -1
  14. package/dist/ai-chat-v5-migration.d.ts +1 -1
  15. package/dist/ai-chat-v5-migration.js +1 -2
  16. package/dist/ai-chat-v5-migration.js.map +1 -1
  17. package/dist/ai-react.d.ts +1 -1
  18. package/dist/ai-react.js +1 -2
  19. package/dist/ai-react.js.map +1 -1
  20. package/dist/ai-types.d.ts +1 -7
  21. package/dist/ai-types.js +2 -8
  22. package/dist/ai-types.js.map +1 -1
  23. package/dist/browser/ai.js +1 -1
  24. package/dist/browser/index.js +1 -1
  25. package/dist/chat/index.d.ts +1918 -72
  26. package/dist/chat/index.js +1730 -245
  27. package/dist/chat/index.js.map +1 -1
  28. package/dist/chat/react.d.ts +602 -0
  29. package/dist/chat/react.js +1506 -0
  30. package/dist/chat/react.js.map +1 -0
  31. package/dist/chat-sdk/index.d.ts +4 -4
  32. package/dist/{classPrivateFieldGet2-CZ7QjTXN.js → classPrivateFieldGet2-DZBYAB34.js} +5 -5
  33. package/dist/{classPrivateMethodInitSpec-D-0__zd9.js → classPrivateMethodInitSpec-qMjJ6sHQ.js} +2 -2
  34. package/dist/{client-BXJ9n2f7.js → client-BZ-B3NhC.js} +2 -2
  35. package/dist/client-BZ-B3NhC.js.map +1 -0
  36. package/dist/client-tools-aIBO0Fk7.d.ts +53 -0
  37. package/dist/client.d.ts +76 -57
  38. package/dist/client.js +33 -5
  39. package/dist/client.js.map +1 -1
  40. package/dist/{connector-CrKhowfD.js → connector-CdldGF3h.js} +5 -5
  41. package/dist/{connector-CrKhowfD.js.map → connector-CdldGF3h.js.map} +1 -1
  42. package/dist/email.js +1 -1
  43. package/dist/email.js.map +1 -1
  44. package/dist/{index-B7IbEeze.d.ts → index-CcbnKkNh.d.ts} +188 -14
  45. package/dist/index.d.ts +91 -71
  46. package/dist/index.js +562 -24
  47. package/dist/index.js.map +1 -1
  48. package/dist/mcp/client.d.ts +18 -14
  49. package/dist/mcp/client.js +1 -1
  50. package/dist/mcp/index.d.ts +30 -30
  51. package/dist/mcp/index.js +7 -7
  52. package/dist/mcp/index.js.map +1 -1
  53. package/dist/{agent-tools-3zLG7MgA.js → message-builder-BymO4N_D.js} +45 -126
  54. package/dist/message-builder-BymO4N_D.js.map +1 -0
  55. package/dist/observability/index.d.ts +1 -1
  56. package/dist/observability/index.js +4 -2
  57. package/dist/observability/index.js.map +1 -1
  58. package/dist/react.d.ts +123 -110
  59. package/dist/react.js +44 -11
  60. package/dist/react.js.map +1 -1
  61. package/dist/serializable.d.ts +1 -1
  62. package/dist/skills/compile.js +1 -1
  63. package/dist/skills/compile.js.map +1 -1
  64. package/dist/skills/index.js +5 -5
  65. package/dist/skills/index.js.map +1 -1
  66. package/dist/sub-routing.d.ts +6 -6
  67. package/dist/utils.js +1 -1
  68. package/dist/utils.js.map +1 -1
  69. package/dist/vite.js +5 -5
  70. package/dist/vite.js.map +1 -1
  71. package/dist/wire-types-nflOzNuU.js +240 -0
  72. package/dist/wire-types-nflOzNuU.js.map +1 -0
  73. package/dist/{workflow-types-SrZK_o9p.d.ts → workflow-types-Baz_PO5v.d.ts} +42 -22
  74. package/dist/workflow-types.d.ts +25 -21
  75. package/dist/workflow-types.js.map +1 -1
  76. package/dist/workflows.d.ts +22 -21
  77. package/dist/workflows.js +31 -7
  78. package/dist/workflows.js.map +1 -1
  79. package/docs/adding-to-existing-project.md +450 -0
  80. package/docs/agent-class.md +503 -0
  81. package/docs/agent-tools.md +552 -0
  82. package/docs/browse-the-web.md +430 -0
  83. package/docs/callable-methods.md +627 -0
  84. package/docs/chat-agents.md +1687 -0
  85. package/docs/chat-sdk.md +181 -0
  86. package/docs/client-sdk.md +520 -0
  87. package/docs/client-tools-continuation.md +177 -0
  88. package/docs/codemode.md +440 -0
  89. package/docs/configuration.md +775 -0
  90. package/docs/cross-domain-authentication.md +171 -0
  91. package/docs/durable-execution.md +537 -0
  92. package/docs/email.md +663 -0
  93. package/docs/get-current-agent.md +204 -0
  94. package/docs/getting-started.md +305 -0
  95. package/docs/http-websockets.md +668 -0
  96. package/docs/human-in-the-loop.md +661 -0
  97. package/docs/index.md +151 -0
  98. package/docs/long-running-agents.md +730 -0
  99. package/docs/mcp-client.md +620 -0
  100. package/docs/mcp-servers.md +526 -0
  101. package/docs/mcp-transports.md +308 -0
  102. package/docs/migration-to-ai-sdk-v5.md +96 -0
  103. package/docs/migration-to-ai-sdk-v6.md +163 -0
  104. package/docs/observability.md +261 -0
  105. package/docs/push-notifications.md +367 -0
  106. package/docs/queue.md +329 -0
  107. package/docs/readonly-connections.md +278 -0
  108. package/docs/resumable-streaming.md +127 -0
  109. package/docs/retries.md +444 -0
  110. package/docs/routing.md +749 -0
  111. package/docs/scheduling.md +898 -0
  112. package/docs/securing-mcp-servers.md +359 -0
  113. package/docs/server-driven-messages.md +477 -0
  114. package/docs/sessions.md +1024 -0
  115. package/docs/state.md +512 -0
  116. package/docs/sub-agents.md +389 -0
  117. package/docs/webhooks.md +604 -0
  118. package/docs/workflows.md +877 -0
  119. package/package.json +41 -14
  120. package/dist/agent-tools-3zLG7MgA.js.map +0 -1
  121. package/dist/agent-tools-DZhI5F6Q.d.ts +0 -14
  122. package/dist/client-BXJ9n2f7.js.map +0 -1
@@ -0,0 +1,177 @@
1
+ # Client-Side Tools and Auto-Continuation
2
+
3
+ ## Overview
4
+
5
+ Tools in `AIChatAgent` can be divided into two categories:
6
+
7
+ - **Server tools**: Have an `execute` function on the server. The AI SDK runs them automatically and the LLM continues responding in the same turn.
8
+ - **Client tools**: No `execute` function on the server. The tool call is sent to the client via `onToolCall`, and the client provides the result. By default, this requires a new request to continue.
9
+
10
+ With `autoContinueAfterToolResult`, client tools can behave like server tools -- the LLM calls a tool, the client executes it, and the server automatically continues the conversation in the same turn.
11
+
12
+ ## Server Setup
13
+
14
+ Define a tool without an `execute` function. The AI SDK will pause and send `tool-input-available` to the client:
15
+
16
+ ```typescript
17
+ import { AIChatAgent } from "@cloudflare/ai-chat";
18
+ import { createWorkersAI } from "workers-ai-provider";
19
+ import { streamText, tool, convertToModelMessages, stepCountIs } from "ai";
20
+ import { z } from "zod";
21
+
22
+ export class MyAgent extends AIChatAgent {
23
+ async onChatMessage() {
24
+ const workersai = createWorkersAI({ binding: this.env.AI });
25
+
26
+ const result = streamText({
27
+ model: workersai("@cf/moonshotai/kimi-k2.7-code"),
28
+ messages: await convertToModelMessages(this.messages),
29
+ tools: {
30
+ // Client-side tool: no execute function
31
+ getUserLocation: tool({
32
+ description: "Get the user's location from their browser",
33
+ inputSchema: z.object({})
34
+ }),
35
+
36
+ // Server-side tool: has execute, runs automatically
37
+ getWeather: tool({
38
+ description: "Get weather for a city",
39
+ inputSchema: z.object({ city: z.string() }),
40
+ execute: async ({ city }) => fetchWeather(city)
41
+ })
42
+ },
43
+ stopWhen: stepCountIs(5) // Allow multi-step so the LLM can respond after tool results
44
+ });
45
+
46
+ return result.toUIMessageStreamResponse();
47
+ }
48
+ }
49
+ ```
50
+
51
+ ## Client Setup
52
+
53
+ Use `onToolCall` to handle client-side tool execution. Auto-continuation is enabled by default (`autoContinueAfterToolResult: true`), so the server automatically calls `onChatMessage()` again after receiving the tool result, letting the LLM continue in the same assistant message.
54
+
55
+ ```tsx
56
+ import { useAgent } from "agents/react";
57
+ import { useAgentChat } from "@cloudflare/ai-chat/react";
58
+
59
+ function Chat() {
60
+ const agent = useAgent({ agent: "MyAgent" });
61
+
62
+ const { messages, sendMessage } = useAgentChat({
63
+ agent,
64
+ // Auto-continuation is enabled by default — no need to set this explicitly
65
+ // autoContinueAfterToolResult: true,
66
+ onToolCall: async ({ toolCall, addToolOutput }) => {
67
+ if (toolCall.toolName === "getUserLocation") {
68
+ const pos = await new Promise((resolve, reject) => {
69
+ navigator.geolocation.getCurrentPosition(resolve, reject);
70
+ });
71
+ addToolOutput({
72
+ toolCallId: toolCall.toolCallId,
73
+ output: {
74
+ lat: pos.coords.latitude,
75
+ lng: pos.coords.longitude
76
+ }
77
+ });
78
+ }
79
+ }
80
+ });
81
+
82
+ // Render messages...
83
+ }
84
+ ```
85
+
86
+ ## How It Works
87
+
88
+ ```
89
+ User: "What's the weather near me?"
90
+
91
+ 1. Client sends message → Server calls LLM
92
+ 2. LLM decides to call getUserLocation (no server execute)
93
+ 3. Stream sends tool-input-available to client
94
+ 4. onToolCall fires → client gets geolocation → sends CF_AGENT_TOOL_RESULT
95
+ 5. Server receives result with autoContinue: true
96
+ 6. Server waits for the original stream to complete
97
+ 7. Server calls onChatMessage() again (continuation)
98
+ 8. LLM sees the location result, calls getWeather (server execute)
99
+ 9. LLM responds: "It's sunny and 72°F near you!"
100
+ 10. Continuation parts are merged into the same assistant message
101
+ ```
102
+
103
+ The user sees a single seamless response, even though it involved a client-side tool call mid-stream.
104
+
105
+ ## Without Auto-Continuation
106
+
107
+ When `autoContinueAfterToolResult` is set to `false`, the client must explicitly send a follow-up message after providing the tool result:
108
+
109
+ ```tsx
110
+ const { messages, sendMessage, addToolOutput } = useAgentChat({
111
+ agent,
112
+ onToolCall: async ({ toolCall, addToolOutput: provide }) => {
113
+ if (toolCall.toolName === "getUserLocation") {
114
+ const pos = await getPosition();
115
+ provide({
116
+ toolCallId: toolCall.toolCallId,
117
+ output: { lat: pos.coords.latitude, lng: pos.coords.longitude }
118
+ });
119
+ }
120
+ }
121
+ autoContinueAfterToolResult: false, // Disable auto-continuation
122
+ });
123
+
124
+ // After tool result is provided, send a follow-up to continue
125
+ // This creates a new assistant message rather than continuing the existing one
126
+ ```
127
+
128
+ Use this when you want explicit control over when the conversation continues, or when tool results need user review before proceeding.
129
+
130
+ ## Combining with `needsApproval`
131
+
132
+ You can use client-side tools and approval together. For example, a tool that needs both user approval and browser execution:
133
+
134
+ ```typescript
135
+ // Server: tool with needsApproval but no execute
136
+ const shareLocation = tool({
137
+ description: "Share the user's location with a third party",
138
+ inputSchema: z.object({ service: z.string() }),
139
+ needsApproval: true
140
+ // No execute - client handles after approval
141
+ });
142
+ ```
143
+
144
+ ```tsx
145
+ // Client: handle approval, then execute
146
+ const { addToolApprovalResponse } = useAgentChat({
147
+ agent,
148
+ autoContinueAfterToolResult: true,
149
+ onToolCall: async ({ toolCall, addToolOutput }) => {
150
+ if (toolCall.toolName === "shareLocation") {
151
+ const pos = await getPosition();
152
+ addToolOutput({
153
+ toolCallId: toolCall.toolCallId,
154
+ output: { lat: pos.coords.latitude, lng: pos.coords.longitude }
155
+ });
156
+ }
157
+ }
158
+ });
159
+ ```
160
+
161
+ The flow becomes: LLM calls tool → user approves → client executes → server auto-continues.
162
+
163
+ If the user denies the tool instead, you can provide a custom error message using `addToolOutput` with `state: "output-error"`:
164
+
165
+ ```tsx
166
+ // Deny with a reason instead of generic rejection
167
+ addToolOutput({
168
+ toolCallId: toolCall.toolCallId,
169
+ state: "output-error",
170
+ errorText: "User declined to share location"
171
+ });
172
+ ```
173
+
174
+ ## Related Docs
175
+
176
+ - [Chat Agents](./chat-agents.md) — Full `AIChatAgent` and `useAgentChat` reference
177
+ - [Human in the Loop](./human-in-the-loop.md) — Approval patterns including `needsApproval`
@@ -0,0 +1,440 @@
1
+ # Codemode (Experimental)
2
+
3
+ Codemode lets LLMs write and execute code that orchestrates your tools, instead of calling them one at a time. Inspired by [CodeAct](https://machinelearning.apple.com/research/codeact), it works because LLMs are better at writing code than making individual tool calls — they have seen millions of lines of real-world TypeScript but only contrived tool-calling examples.
4
+
5
+ The `@cloudflare/codemode` package converts your tools into typed TypeScript APIs, gives the LLM a single "write code" tool, and executes the generated code in a secure, isolated Worker sandbox.
6
+
7
+ > **Experimental** — this feature may have breaking changes in future releases. Use with caution in production.
8
+
9
+ ## When to use Codemode
10
+
11
+ Codemode is most useful when the LLM needs to:
12
+
13
+ - **Chain multiple tool calls** with logic between them (conditionals, loops, error handling)
14
+ - **Compose results** from different tools before returning
15
+ - **Work with MCP servers** that expose many fine-grained operations
16
+ - **Perform multi-step workflows** that would require many round-trips with standard tool calling
17
+
18
+ For simple, single tool calls, standard AI SDK tool calling is simpler and sufficient.
19
+
20
+ ## Installation
21
+
22
+ ```sh
23
+ npm install @cloudflare/codemode ai zod
24
+ ```
25
+
26
+ ## Quick start
27
+
28
+ ### 1. Define your tools
29
+
30
+ Use the standard AI SDK `tool()` function:
31
+
32
+ ```typescript
33
+ import { tool } from "ai";
34
+ import { z } from "zod";
35
+
36
+ const tools = {
37
+ getWeather: tool({
38
+ description: "Get weather for a location",
39
+ inputSchema: z.object({ location: z.string() }),
40
+ execute: async ({ location }) => `Weather in ${location}: 72°F, sunny`
41
+ }),
42
+ sendEmail: tool({
43
+ description: "Send an email",
44
+ inputSchema: z.object({
45
+ to: z.string(),
46
+ subject: z.string(),
47
+ body: z.string()
48
+ }),
49
+ execute: async ({ to, subject, body }) => `Email sent to ${to}`
50
+ })
51
+ };
52
+ ```
53
+
54
+ ### 2. Create the codemode tool
55
+
56
+ `createCodeTool` takes your tools and an executor, and returns a single AI SDK tool:
57
+
58
+ ```typescript
59
+ import { createCodeTool } from "@cloudflare/codemode/ai";
60
+ import { DynamicWorkerExecutor } from "@cloudflare/codemode";
61
+
62
+ const executor = new DynamicWorkerExecutor({
63
+ loader: env.LOADER
64
+ });
65
+
66
+ const codemode = createCodeTool({ tools, executor });
67
+ ```
68
+
69
+ ### 3. Use it with streamText
70
+
71
+ Pass the codemode tool to `streamText` or `generateText` like any other tool. You choose the model:
72
+
73
+ ```typescript
74
+ import { streamText } from "ai";
75
+
76
+ const result = streamText({
77
+ model,
78
+ system: "You are a helpful assistant.",
79
+ messages,
80
+ tools: { codemode }
81
+ });
82
+ ```
83
+
84
+ When the LLM decides to use codemode, it writes an async arrow function like:
85
+
86
+ ```javascript
87
+ async () => {
88
+ const weather = await codemode.getWeather({ location: "London" });
89
+ if (weather.includes("sunny")) {
90
+ await codemode.sendEmail({
91
+ to: "team@example.com",
92
+ subject: "Nice day!",
93
+ body: `It's ${weather}`
94
+ });
95
+ }
96
+ return { weather, notified: true };
97
+ };
98
+ ```
99
+
100
+ The code runs in an isolated Worker sandbox, tool calls are dispatched back to the host via Workers RPC, and the result is returned to the LLM.
101
+
102
+ ## Configuration
103
+
104
+ ### Wrangler bindings
105
+
106
+ Add a `worker_loaders` binding to your `wrangler.jsonc`. This is the only binding required:
107
+
108
+ ```jsonc
109
+ // wrangler.jsonc
110
+ {
111
+ "worker_loaders": [{ "binding": "LOADER" }],
112
+ "compatibility_flags": ["nodejs_compat"]
113
+ }
114
+ ```
115
+
116
+ ### Vite configuration
117
+
118
+ If you use `zod-to-ts` (which codemode depends on), add a `__filename` define to your Vite config:
119
+
120
+ ```typescript
121
+ // vite.config.ts
122
+ export default defineConfig({
123
+ plugins: [react(), cloudflare(), tailwindcss()],
124
+ define: {
125
+ __filename: "'index.ts'"
126
+ }
127
+ });
128
+ ```
129
+
130
+ ## How it works
131
+
132
+ ```
133
+ ┌─────────────┐ ┌──────────────────────────────────────┐
134
+ │ │ │ Dynamic Worker (isolated sandbox) │
135
+ │ Host │ RPC │ │
136
+ │ Worker │◄──────►│ LLM-generated code runs here │
137
+ │ │ │ codemode.myTool() → dispatcher.call()│
138
+ │ ToolDispatcher │ │
139
+ │ holds tool fns │ fetch() blocked by default │
140
+ └─────────────┘ └──────────────────────────────────────┘
141
+ ```
142
+
143
+ 1. `createCodeTool` generates TypeScript type definitions from your tools and builds a description the LLM can read
144
+ 2. The LLM writes an async arrow function that calls `codemode.toolName(args)`
145
+ 3. The code is normalized via AST parsing (acorn) and sent to the executor
146
+ 4. `DynamicWorkerExecutor` spins up an isolated Worker via `WorkerLoader`
147
+ 5. Inside the sandbox, a `Proxy` intercepts `codemode.*` calls and routes them back to the host via Workers RPC (`ToolDispatcher extends RpcTarget`)
148
+ 6. Console output (`console.log`, `console.warn`, `console.error`) is captured and returned in the result
149
+
150
+ ### Network isolation
151
+
152
+ External `fetch()` and `connect()` are **blocked by default** — enforced at the Workers runtime level via `globalOutbound: null`. Sandboxed code can only interact with the host through `codemode.*` tool calls.
153
+
154
+ To allow controlled outbound access, pass a `Fetcher`:
155
+
156
+ ```typescript
157
+ const executor = new DynamicWorkerExecutor({
158
+ loader: env.LOADER,
159
+ globalOutbound: null // default — fully isolated
160
+ // globalOutbound: env.MY_OUTBOUND_SERVICE // route through a Fetcher
161
+ });
162
+ ```
163
+
164
+ ## Using with an Agent
165
+
166
+ The typical pattern is to create the executor and codemode tool inside an Agent's message handler:
167
+
168
+ ```typescript
169
+ import { Agent } from "agents";
170
+ import { createCodeTool } from "@cloudflare/codemode/ai";
171
+ import { DynamicWorkerExecutor } from "@cloudflare/codemode";
172
+ import { streamText, convertToModelMessages, stepCountIs } from "ai";
173
+
174
+ export class MyAgent extends Agent<Env, State> {
175
+ async onChatMessage() {
176
+ const executor = new DynamicWorkerExecutor({
177
+ loader: this.env.LOADER
178
+ });
179
+
180
+ const codemode = createCodeTool({
181
+ tools: myTools,
182
+ executor
183
+ });
184
+
185
+ const result = streamText({
186
+ model,
187
+ system: "You are a helpful assistant.",
188
+ messages: await convertToModelMessages(this.state.messages),
189
+ tools: { codemode },
190
+ stopWhen: stepCountIs(10)
191
+ });
192
+
193
+ // Stream response back to client...
194
+ }
195
+ }
196
+ ```
197
+
198
+ ### With MCP tools
199
+
200
+ MCP tools work the same way — merge them into the tool set:
201
+
202
+ ```typescript
203
+ const codemode = createCodeTool({
204
+ tools: {
205
+ ...myTools,
206
+ ...this.mcp.getAITools()
207
+ },
208
+ executor
209
+ });
210
+ ```
211
+
212
+ Tool names with hyphens or dots (common in MCP) are automatically sanitized to valid JavaScript identifiers (e.g., `my-server.list-items` becomes `my_server_list_items`).
213
+
214
+ ### Browser executor with dynamic client tools
215
+
216
+ If your tools live in the browser instead of the Agent, build codemode from
217
+ those browser-side functions and register it with whatever client-tool layer
218
+ you already use. This keeps the server generic while running generated code in
219
+ an iframe sandbox on the page.
220
+
221
+ **Server:**
222
+
223
+ ```typescript
224
+ import { AIChatAgent, createToolsFromClientSchemas } from "@cloudflare/ai-chat";
225
+ import { createWorkersAI } from "workers-ai-provider";
226
+ import { streamText, convertToModelMessages, stepCountIs } from "ai";
227
+
228
+ export class BrowserCodemodeAgent extends AIChatAgent<Env> {
229
+ async onChatMessage(_onFinish, options) {
230
+ const workersai = createWorkersAI({ binding: this.env.AI });
231
+
232
+ const result = streamText({
233
+ model: workersai("@cf/moonshotai/kimi-k2.7-code"),
234
+ messages: await convertToModelMessages(this.messages),
235
+ tools: createToolsFromClientSchemas(options?.clientTools),
236
+ stopWhen: stepCountIs(10)
237
+ });
238
+
239
+ return result.toUIMessageStreamResponse();
240
+ }
241
+ }
242
+ ```
243
+
244
+ **Client:**
245
+
246
+ ```tsx
247
+ import { useAgent } from "agents/react";
248
+ import { useAgentChat, type AITool } from "@cloudflare/ai-chat/react";
249
+ import {
250
+ IframeSandboxExecutor,
251
+ createBrowserCodeTool
252
+ } from "@cloudflare/codemode/browser";
253
+
254
+ const browserTools = {
255
+ getPageTitle: {
256
+ description: "Get the current page title",
257
+ inputSchema: {
258
+ type: "object",
259
+ properties: {},
260
+ required: []
261
+ },
262
+ execute: async () => ({ title: document.title })
263
+ },
264
+ getSelectionText: {
265
+ description: "Get the user's current text selection",
266
+ inputSchema: {
267
+ type: "object",
268
+ properties: {},
269
+ required: []
270
+ },
271
+ execute: async () => ({
272
+ text: window.getSelection()?.toString() ?? ""
273
+ })
274
+ }
275
+ };
276
+
277
+ const codemode = createBrowserCodeTool({
278
+ tools: browserTools,
279
+ executor: new IframeSandboxExecutor()
280
+ });
281
+
282
+ function BrowserCodemodeChat() {
283
+ const agent = useAgent({ agent: "BrowserCodemodeAgent" });
284
+
285
+ const tools: Record<string, AITool> = {
286
+ codemode: {
287
+ description: codemode.description,
288
+ parameters: codemode.inputSchema,
289
+ execute: codemode.execute
290
+ }
291
+ };
292
+
293
+ const { messages, sendMessage } = useAgentChat({
294
+ agent,
295
+ tools,
296
+ onToolCall: async ({ toolCall, addToolOutput }) => {
297
+ const tool = tools[toolCall.toolName];
298
+ if (tool?.execute) {
299
+ const output = await tool.execute(toolCall.input);
300
+ addToolOutput({ toolCallId: toolCall.toolCallId, output });
301
+ }
302
+ }
303
+ });
304
+
305
+ // Render your chat UI...
306
+ }
307
+ ```
308
+
309
+ This pattern is useful when:
310
+
311
+ - the browser owns the tool surface at runtime
312
+ - your page exposes client-side capabilities that only the browser can run
313
+ - you want codemode's typed code-generation prompt without routing tool
314
+ execution through the server
315
+
316
+ If your browser tools are dynamic, rebuild the codemode descriptor whenever the
317
+ tool set changes and re-register it with your client tool layer. Codemode stays
318
+ agnostic about where those tools come from.
319
+
320
+ If you need approval-gated tools, use the standard `needsApproval` +
321
+ `useAgentChat` approval flow described in
322
+ [Human in the Loop](https://github.com/cloudflare/agents/blob/main/docs/agents/human-in-the-loop.md). Codemode currently excludes tools
323
+ with `needsApproval` instead of pausing execution for approval.
324
+
325
+ ## The Executor interface
326
+
327
+ The `Executor` interface is deliberately minimal — implement it to run code in any sandbox:
328
+
329
+ ```typescript
330
+ interface Executor {
331
+ execute(
332
+ code: string,
333
+ providersOrFns:
334
+ | ResolvedProvider[]
335
+ | Record<string, (...args: unknown[]) => Promise<unknown>>
336
+ ): Promise<ExecuteResult>;
337
+ }
338
+
339
+ interface ResolvedProvider {
340
+ name: string;
341
+ fns: Record<string, (...args: unknown[]) => Promise<unknown>>;
342
+ positionalArgs?: boolean;
343
+ }
344
+
345
+ interface ExecuteResult {
346
+ result: unknown;
347
+ error?: string;
348
+ logs?: string[];
349
+ }
350
+ ```
351
+
352
+ `DynamicWorkerExecutor` is the built-in Cloudflare Workers implementation. You can build your own for Node VM, QuickJS, containers, or any other sandbox.
353
+
354
+ ## API reference
355
+
356
+ ### `createCodeTool(options)`
357
+
358
+ Returns an AI SDK compatible `Tool`.
359
+
360
+ | Option | Type | Default | Description |
361
+ | ------------- | ---------------------------- | -------------- | ------------------------------------------------------ |
362
+ | `tools` | `ToolSet \| ToolDescriptors` | required | Your tools (AI SDK `tool()` or raw descriptors) |
363
+ | `executor` | `Executor` | required | Where to run the generated code |
364
+ | `description` | `string` | auto-generated | Custom tool description. Use `{{types}}` for type defs |
365
+
366
+ ### `DynamicWorkerExecutor`
367
+
368
+ Executes code in an isolated Cloudflare Worker via `WorkerLoader`.
369
+
370
+ | Option | Type | Default | Description |
371
+ | ---------------- | ----------------- | -------- | ------------------------------------------------------------ |
372
+ | `loader` | `WorkerLoader` | required | Worker Loader binding from `env.LOADER` |
373
+ | `timeout` | `number` | `60000` | Execution timeout in ms |
374
+ | `globalOutbound` | `Fetcher \| null` | `null` | Network access control. `null` = blocked, `Fetcher` = routed |
375
+
376
+ ### `IframeSandboxExecutor`
377
+
378
+ Executes code in a sandboxed browser iframe. Import it from
379
+ `@cloudflare/codemode/browser`.
380
+
381
+ | Option | Type | Default | Description |
382
+ | --------- | -------- | --------------------------------------------------------------- | ------------------------------------------------------------------------ |
383
+ | `timeout` | `number` | `30000` | Execution timeout in ms. Cannot preempt tight synchronous browser loops. |
384
+ | `csp` | `string` | `default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval';` | Content Security Policy applied to the sandbox iframe document. |
385
+
386
+ ### `generateTypes(tools)`
387
+
388
+ Generates TypeScript type definitions from your tools. Used internally by `createCodeTool` but exported for custom use (e.g., displaying types in a frontend).
389
+
390
+ ```typescript
391
+ import { generateTypes } from "@cloudflare/codemode";
392
+
393
+ const types = generateTypes(myTools);
394
+ // Returns:
395
+ // type CreateProjectInput = { name: string; description?: string }
396
+ // declare const codemode: { createProject: (input: CreateProjectInput) => Promise<unknown>; }
397
+ ```
398
+
399
+ ### `sanitizeToolName(name)`
400
+
401
+ Converts tool names into valid JavaScript identifiers.
402
+
403
+ ```typescript
404
+ import { sanitizeToolName } from "@cloudflare/codemode";
405
+
406
+ sanitizeToolName("get-weather"); // "get_weather"
407
+ sanitizeToolName("3d-render"); // "_3d_render"
408
+ sanitizeToolName("delete"); // "delete_"
409
+ ```
410
+
411
+ ## Security considerations
412
+
413
+ - Code runs in **isolated Worker sandboxes** — each execution gets its own Worker instance
414
+ - External network access (`fetch`, `connect`) is **blocked by default** at the runtime level
415
+ - Tool calls are dispatched via Workers RPC, not network requests
416
+ - Execution has a configurable **timeout** (default 60 seconds)
417
+ - Console output is captured separately and does not leak to the host
418
+ - Browser iframe execution runs in a sandboxed iframe with a restrictive CSP by
419
+ default. It uses nonce-scoped internal messages, but its timeout cannot preempt
420
+ tight synchronous loops like `while (true) {}` because those block the browser
421
+ event loop.
422
+
423
+ ## Current limitations
424
+
425
+ - **Tool approval (`needsApproval`) is not supported yet.** Tools with
426
+ `needsApproval: true` or a `needsApproval` function are excluded from codemode
427
+ instead of pausing execution for approval. Support for approval flows within
428
+ codemode is planned. For now, use approval-required tools through standard AI
429
+ SDK tool calling instead.
430
+ - Requires Cloudflare Workers environment for `DynamicWorkerExecutor`
431
+ - Limited to JavaScript execution
432
+ - The `zod-to-ts` dependency bundles the TypeScript compiler, which increases Worker size
433
+ - LLM code quality depends on prompt engineering and model capability
434
+
435
+ ## Example
436
+
437
+ See [`examples/codemode/`](https://github.com/cloudflare/agents/tree/main/examples/codemode) for a full working example — a project management assistant that uses codemode to orchestrate tasks, sprints, and comments via SQLite.
438
+
439
+ See [`examples/codemode-browser/`](https://github.com/cloudflare/agents/tree/main/examples/codemode-browser) for a browser
440
+ iframe executor example with dynamic client tools.