diff --git a/public/__redirects b/public/__redirects index e6987de3b87..6d1339eaeb8 100644 --- a/public/__redirects +++ b/public/__redirects @@ -258,8 +258,9 @@ /agents/runtime/communication/readonly-connections/ /agents/agent-class/readonly-connections/ 301 /agents/runtime/communication/routing/ /agents/agent-class/routing/ 301 /agents/runtime/communication/websockets/ /agents/agent-class/websockets/ 301 -/agents/runtime/execution/ /agents/skills/ 301 -/agents/runtime/execution/agent-skills/ /agents/skills/execution/ 301 +/agents/runtime/execution/ /agents/runtime/code-mode/ 301 +/agents/runtime/execution/agent-skills/ /agents/runtime/code-mode/skill-scripts/ 301 +/agents/skills/execution/ /agents/runtime/code-mode/skill-scripts/ 301 /agents/runtime/execution/agent-tools/ /agents/agent-class/agent-tools/ 301 /agents/runtime/execution/durable-execution/ /agents/agent-class/durable-execution/ 301 /agents/runtime/execution/queue-tasks/ /agents/agent-class/queue-tasks/ 301 @@ -317,7 +318,7 @@ /agents/api-reference/callable-methods/ /agents/agent-class/callable-methods/ 301 /agents/api-reference/chat-agents/ /agents/agent-class/communication-channels/chat/chat-agents/ 301 /agents/api-reference/client-sdk/ /agents/agent-class/communication-channels/chat/client-sdk/ 301 -/agents/api-reference/codemode/ /agents/tools/codemode/api-reference/ 301 +/agents/api-reference/codemode/ /agents/runtime/code-mode/legacy/ 301 /agents/api-reference/configuration/ /agents/runtime/routing/ 301 /agents/api-reference/durable-execution/ /agents/agent-class/durable-execution/ 301 /agents/api-reference/email/ /agents/agent-class/communication-channels/email/ 301 @@ -358,9 +359,10 @@ /agents/guides/test-remote-mcp-server/ /agents/model-context-protocol/guides/test-remote-mcp-server/ 301 /agents/guides/webhooks/ /agents/agent-class/communication-channels/webhooks/ 301 /agents/model-context-protocol/authorization/ /agents/model-context-protocol/protocol/authorization/ 301 -/agents/model-context-protocol/protocol/codemode/ /agents/tools/codemode/ 301 -/agents/model-context-protocol/codemode/ /agents/model-context-protocol/guides/build-codemode-mcp-server/ 301 -/agents/model-context-protocol/guides/build-codemode-openapi-mcp-server/ /agents/model-context-protocol/guides/build-codemode-mcp-server/ 301 +/agents/model-context-protocol/protocol/codemode/ /agents/concepts/code-mode/ 301 +/agents/model-context-protocol/codemode/ /agents/concepts/code-mode/ 301 +/agents/model-context-protocol/guides/build-codemode-openapi-mcp-server/ /agents/concepts/code-mode/ 301 +/agents/model-context-protocol/guides/build-codemode-mcp-server/ /agents/concepts/code-mode/ 301 /agents/model-context-protocol/governance/ /agents/model-context-protocol/protocol/governance/ 301 /agents/model-context-protocol/mcp-portal/ /agents/model-context-protocol/cloudflare/mcp-portal/ 301 /agents/model-context-protocol/mcp-servers-for-cloudflare/ /agents/model-context-protocol/cloudflare/servers-for-cloudflare/ 301 @@ -395,7 +397,16 @@ /agents/communication-channels/voice/build-a-voice-agent/ /agents/examples/voice-agent/ 301 /agents/communication-channels/slack/slack-agent/ /agents/examples/slack-agent/ 301 /agents/tools/browser/browse-the-web/ /agents/tools/browser/ 301 -/agents/tools/sandbox/codemode/ /agents/tools/codemode/ 301 +/agents/tools/sandbox/codemode/ /agents/concepts/code-mode/ 301 +/agents/tools/codemode/ /agents/concepts/code-mode/ 301 +/agents/tools/codemode/how-it-works/ /agents/concepts/code-mode/ 301 +/agents/tools/codemode/ai-sdk/ /agents/runtime/code-mode/computer/ 301 +/agents/tools/codemode/tanstack-ai/ /agents/runtime/code-mode/computer/ 301 +/agents/tools/codemode/mcp/ /agents/runtime/code-mode/computer/ 301 +/agents/tools/codemode/openapi/ /agents/runtime/code-mode/computer/ 301 +/agents/tools/codemode/durable-runtime/ /agents/runtime/code-mode/legacy/ 301 +/agents/tools/codemode/browser/ /agents/runtime/code-mode/legacy/ 301 +/agents/tools/codemode/api-reference/ /agents/runtime/code-mode/legacy/ 301 /agents/tools/payments/mpp/charge-for-http-content/ /agents/tools/payments/mpp-charge-for-http-content/ 301 /agents/tools/payments/mpp/protect-an-origin/ /agents/tools/payments/mpp-charge-for-http-content/ 301 /agents/agentic-payments/mpp/charge-for-http-content/ /agents/tools/payments/mpp-charge-for-http-content/ 301 diff --git a/src/content/docs/agents/concepts/agent-skills.mdx b/src/content/docs/agents/concepts/agent-skills.mdx index fec110d1daa..4b2151eaff7 100644 --- a/src/content/docs/agents/concepts/agent-skills.mdx +++ b/src/content/docs/agents/concepts/agent-skills.mdx @@ -127,7 +127,7 @@ The Agents SDK reads the open format in `agents/skills` and checks every `SKILL. | [R2](/agents/skills/sources/r2/) | Many agents share one set of skills managed outside your Worker | | [Code](/agents/skills/sources/code/) | Tests, or a small fixed set of skills that ships with your Worker | -The SDK uses the `.agents/skills/` folder convention that several agents share, so `/workspace/.agents/skills/` in a Workspace holds the agent's skills. The model loads skills with the [skill tools](/agents/tools/skills/) `activate_skill` and `read_skill_resource`. With a Workspace backend, the model runs skill scripts with the Workspace `exec` tool. Refer to [Execution](/agents/skills/execution/). +The SDK uses the `.agents/skills/` folder convention that several agents share, so `/workspace/.agents/skills/` in a Workspace holds the agent's skills. The model loads skills with the [skill tools](/agents/tools/skills/) `activate_skill` and `read_skill_resource`. With a Workspace backend, the model runs skill scripts with the Workspace `exec` tool. Refer to [Run skill scripts](/agents/runtime/code-mode/skill-scripts/). ## Related diff --git a/src/content/docs/agents/concepts/agentic-patterns/human-in-the-loop.mdx b/src/content/docs/agents/concepts/agentic-patterns/human-in-the-loop.mdx index 69499d5b5a4..853681c1afc 100644 --- a/src/content/docs/agents/concepts/agentic-patterns/human-in-the-loop.mdx +++ b/src/content/docs/agents/concepts/agentic-patterns/human-in-the-loop.mdx @@ -3,7 +3,7 @@ title: Human-in-the-loop patterns pcx_content_type: how-to sidebar: order: 2 -description: Implement human-in-the-loop functionality using Workflow approvals, durable Code Mode approvals, and MCP elicitation. +description: Implement human-in-the-loop functionality using Workflow approvals, Code Mode approvals, and MCP elicitation. products: - agents --- @@ -31,7 +31,7 @@ Choose the pattern based on who introduces the pause and where it occurs: | ---------------------- | ------------------------------------------ | --------------------------- | ----------------------- | ------------------------------------------- | | **MCP elicitation** | MCP request handled by your Agent client | MCP server developer | Minutes | `configureElicitationHandlers()` | | **Workflow approval** | Durable application task or tool operation | Agent application developer | Months or years | `waitForApproval()` | -| **Code Mode approval** | Connector call in model-generated code | Code Mode Agent developer | Until configured expiry | `requiresApproval`, `approve()`, `reject()` | +| **Code Mode approval** | Host module call in model-generated code | Code Mode Agent developer | Until answered | `ctx.interrupt()`, `runtime.respond()` | ## Workflow-based approval @@ -362,107 +362,36 @@ export class MyAgent extends Agent { For the complete form, URL, and browser forwarding patterns, refer to [MCP client elicitation](/agents/model-context-protocol/apis/client-api/#elicitation). For the server side, refer to [Elicitation with a stateless handler](/agents/model-context-protocol/apis/handler-api/#elicitation-with-a-stateless-handler). -## Durable Code Mode approvals +## Code Mode approvals -Use the [durable Code Mode runtime](/agents/tools/codemode/durable-runtime/) for coding agents and other Agents that use the Code Mode pattern. Mark a connector method with `requiresApproval: true` to pause model-generated code before it invokes the underlying tool. - -The following example marks a GitHub MCP tool as approval-gated, adds the connector to a durable runtime, and exposes methods that a UI can use to inspect, approve, or reject the pending action: +When an agent uses [Code Mode](/agents/concepts/code-mode/), the model's code calls your tools through host modules. With [Computer](/agents/runtime/code-mode/computer/), a host module function can pause the run until a person answers. The code calls the function as usual and gets its result once the answer arrives. ```ts -import { AIChatAgent } from "@cloudflare/ai-chat"; -import { - createCodemodeRuntime, - DynamicWorkerExecutor, - McpConnector, - type CodemodeRuntimeHandle, - type ConnectorTool, - type McpConnectionLike, - type PendingAction, -} from "@cloudflare/codemode"; -import { callable } from "agents"; -import { convertToModelMessages, stepCountIs, streamText } from "ai"; -import { model } from "./model"; - -class GitHubConnector extends McpConnector { - private connection: McpConnectionLike; - - constructor( - ctx: DurableObjectState, - env: Env, - connection: McpConnectionLike, - ) { - super(ctx, env); - this.connection = connection; - } - - override name() { - return "github"; - } - - protected override createConnection() { - return this.connection; - } - - protected override tool(name: string, tool: ConnectorTool): ConnectorTool { - if (name === "create_issue") { - return { ...tool, requiresApproval: true }; - } - return tool; - } -} - -export class CodingAgent extends AIChatAgent { - private runtime(): CodemodeRuntimeHandle { - const server = this.mcp - .listServers() - .find((item) => item.name === "GitHub"); - if (!server) throw new Error("GitHub MCP server is not registered."); - - const connection = this.mcp.mcpConnections[server.id]; - if (!connection) throw new Error("GitHub MCP connection is unavailable."); - - return createCodemodeRuntime({ - ctx: this.ctx, - executor: new DynamicWorkerExecutor({ loader: this.env.LOADER }), - connectors: [new GitHubConnector(this.ctx, this.env, connection)], - }); - } - - async onChatMessage() { - const result = streamText({ - model, - messages: await convertToModelMessages(this.messages), - tools: { codemode: this.runtime().tool() }, - stopWhen: stepCountIs(10), - }); - - return result.toUIMessageStreamResponse(); - } - - @callable() - async pendingApprovals(): Promise { - return this.runtime().pending(); - } - - @callable() - async approveExecution(executionId: string) { - return this.runtime().approve({ executionId }); - } - - @callable() - async rejectExecution(executionId: string, seq: number): Promise { - return this.runtime().reject({ executionId, seq }); - } -} +new WorkerJavaScriptBackend({ + loader: this.env.LOADER, + onInterrupt: "replay", + modules: { + "codemode:github": { + createIssue: async ([repo, title], ctx) => { + const { approved } = await ctx.interrupt<{ approved: boolean }>({ + repo, + title, + }); + if (!approved) throw new Error("The issue was rejected."); + return this.createIssue(String(repo), String(title), { + idempotencyKey: ctx.callId, + }); + }, + }, + }, +}); ``` -When generated code calls `github.create_issue()`, the runtime records the pending method and arguments, then pauses before the MCP tool executes. Approval starts another pass with the same source code and execution ID. Previously completed calls replay from the durable log, the approved call executes, and the generated code continues. - -Pending approvals and execution history survive request completion and Durable Object hibernation. For the execution and replay model, refer to [Approvals through abort and replay](/agents/tools/codemode/how-it-works/#approvals-through-abort-and-replay). +A UI lists the waiting calls with `workspace.runtime.pending()` and answers one with `workspace.runtime.respond()`. The run survives restarts while it waits. On approval, it replays from a journal to the waiting call, and continues. For the details and current limits, refer to [Ask for approval](/agents/runtime/code-mode/computer/#ask-for-approval). ## Build Workflow approval UIs @@ -593,7 +522,7 @@ class MultiApprovalAgent extends Agent { /> diff --git a/src/content/docs/agents/concepts/code-mode.mdx b/src/content/docs/agents/concepts/code-mode.mdx new file mode 100644 index 00000000000..86b5050c0a1 --- /dev/null +++ b/src/content/docs/agents/concepts/code-mode.mdx @@ -0,0 +1,123 @@ +--- +title: Code Mode +description: Why agents on Cloudflare let the model write code that calls tools, instead of calling each tool one at a time. +pcx_content_type: concept +sidebar: + order: 4.5 +tags: + - AI + - MCP +products: + - agents +--- + +import { LinkCard } from "~/components"; + +Code Mode means letting the model write code to get work done. The model does not ask for one tool call, wait for the result, and then ask for the next. It writes a short program that calls the tools, loops over what comes back, keeps what matters, and returns an answer. A sandbox runs the program, and only the result goes back to the model. + +Any time an agent runs code that a model wrote, it is using Code Mode. This page covers why that works well. To set it up, refer to [Code Mode in the runtime](/agents/runtime/code-mode/). + +## Models are better at writing code + +A tool call is a special token sequence that a model learns during fine-tuning, from a fairly small set of examples. Code is different. Models have trained on huge amounts of real programs, written by people solving real problems. They know how to call functions, read a type signature, write a loop, and handle an error. + +Code Mode gives the model the task in the form it knows best. The tools become functions it can import, and the model writes ordinary JavaScript against them. For the longer argument, refer to [Code Mode: the better way to use MCP](https://blog.cloudflare.com/code-mode/). + +## Code is a compact plan + +Say a user asks for every blocked task across their active projects. With direct tool calls, the model lists the projects, reads the result, and then lists the tasks of each project in turn. Each step is a round trip through the model, and every intermediate result lands in its context. + +With Code Mode, the model writes the whole plan once: + +```js +import { listProjects, listTasks } from "codemode:projects"; + +export default async function () { + const blocked = []; + for (const project of await listProjects("active")) { + const tasks = await listTasks(project.id); + blocked.push(...tasks.filter((task) => task.status === "blocked")); + } + return blocked.map((task) => `${task.projectName}: ${task.title}`); +} +``` + +The program runs in one step and returns a short list. This changes a few things: + +- **Fewer model turns.** Ten projects take one model turn, not eleven. +- **Less context.** The project and task records stay inside the sandbox. The model sees only the list it asked for. +- **Real control flow.** Loops, branches, `try`/`catch`, and `Promise.all()` work as the model expects. It does not have to fake them with a chain of tool calls. +- **A plan you can read.** The code says exactly what the agent did. You can log it, review it, and save it as a file to run again. + +## Tools become modules + +With direct tool calls, the agent sends the definition of every tool to the model on every turn. That cost grows with each tool you add. The Cloudflare API has more than 2,500 endpoints. Described as one tool each, their definitions would take about 1.17 million tokens, more than most models can read at once. + +With Code Mode, tools are exports of a module that the code imports. The model gets one code tool and a short list of the modules it can import. Adding a tool adds an export, not another entry in every prompt. This is why an agent with Code Mode can have far more tools than an agent that calls them directly. + +## The sandbox and the tools stay apart + +Model-written code runs in an isolated [Dynamic Worker](/dynamic-workers/). It starts in milliseconds, and each run gets a fresh isolate. By default it has no network access, and it cannot read the agent's environment variables, bindings, or storage. + +The tools do not run in the sandbox. When code calls `listTasks()`, the call crosses to the agent, and the function runs there with the agent's credentials. The sandbox sees only the arguments and the result. API keys and OAuth tokens never enter the sandbox, and they never enter the model's context. + +## Code Mode in the agent + +The agent can run the code itself. In MCP terms, this is Code Mode on the client: the agent owns the sandbox, so it can combine tools from any source in one program. That includes its own functions, the tools of every MCP server it is connected to, files in its workspace, and external APIs. + +```mermaid +flowchart LR + Model["Model"] -->|"exec: code"| Agent + subgraph Agent["Agent (Durable Object)"] + Sandbox["Dynamic Worker
runs the code"] + Tools["Tools as codemode:* modules"] + Sandbox -->|"import and call"| Tools + end + Tools --> Own["Your functions"] + Tools --> MCP["MCP servers"] + Tools --> APIs["APIs and bindings"] +``` + +This is the recommended way to use Code Mode. [Cloudflare Computer](/agents/runtime/code-mode/computer/) gives the model an `exec` tool, and you give the code your tools as `codemode:*` modules. For example, `codemode:tools` exposes every tool the agent already has, and `codemode:projects` could expose your own functions. + +## Code Mode on the server + +An MCP server can also do Code Mode for its clients, so that a client without its own sandbox still gets the benefit. The [Cloudflare API MCP server](/agents/model-context-protocol/cloudflare/servers-for-cloudflare/) works this way. It exposes two tools, `search` and `execute`. The model writes code against the OpenAPI document to find the endpoints it needs, then writes code against an API client to call them. + +| Approach | Tools | Token cost | +| ---------------------------------- | ----- | ----------- | +| Native MCP (full schemas) | 2,594 | ~1,170,000 | +| Native MCP (required params only) | 2,594 | ~244,000 | +| Code Mode with `search`, `execute` | 2 | ~1,000 | + +Server-side Code Mode composes only that server's tools. When the agent does Code Mode itself, one program can call tools from many servers at once. For the design of the Cloudflare server, refer to [Code Mode: give agents an entire API in 1,000 tokens](https://blog.cloudflare.com/code-mode-mcp/). For code to start from, refer to the [Cloudflare MCP server source](https://github.com/cloudflare/mcp) and the [search and execute example](https://github.com/cloudflare/agents/tree/main/examples/codemode-mcp-openapi). + +## When to call tools directly + +Code Mode is not always the better choice. Call tools directly when: + +- The agent has a few tools, and most tasks need one call. +- A tool needs a person to approve each call before it runs. +- A tool runs in the browser, such as one that reads the user's location. + +Many agents do both. They offer a handful of direct tools, plus a code tool for work that needs several calls. + +## Related resources + + + + + + diff --git a/src/content/docs/agents/concepts/tools.mdx b/src/content/docs/agents/concepts/tools.mdx index f3e4adc0309..75c7b30c720 100644 --- a/src/content/docs/agents/concepts/tools.mdx +++ b/src/content/docs/agents/concepts/tools.mdx @@ -14,7 +14,7 @@ A travel agent might use tools to search flights, check hotel rates, process pay Tool design involves three independent choices: -1. **Model interface** — expose tools as direct tool calls or through Code Mode. +1. **Model interface** — expose tools as direct tool calls, or as modules that model-written code imports with Code Mode. 2. **Execution location** — run tool implementations in a Worker, browser, or another Agent. 3. **Tool source** — define tools in the application or connect to externally hosted tools through Model Context Protocol (MCP). @@ -27,7 +27,7 @@ Direct tool calls and Code Mode define how the model sees and invokes tools. The | Interface | How it works | Use when | | ----------------- | ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- | | Direct tool calls | The model receives individual tool definitions. Each result returns to the model before it chooses the next call. | The task is simple and uses a small, known tool set. | -| Code Mode | The model receives one code tool and writes code against typed tool interfaces. | The task needs composition, dependent calls, filtering, branching, repeatable logic, or tool discovery. | +| Code Mode | The model receives one code tool and writes code that imports tools as modules. | The task needs composition, dependent calls, filtering, branching, or many tools. | ### Direct tool calls @@ -37,17 +37,15 @@ The model sees each intermediate result before choosing another tool. This makes ### Code Mode -[Code Mode](/agents/tools/codemode/) gives the model one code-execution tool. The model writes JavaScript that calls typed tools, passes results between them, and applies control flow. +[Code Mode](/agents/concepts/code-mode/) gives the model one code-execution tool. The model writes JavaScript that imports tools as modules, passes results between them, and applies control flow. -The generated code can filter large responses and return only the final value the model needs. Intermediate results stay inside the sandbox instead of entering the model context after every operation. This makes Code Mode more efficient for composed workflows and large tool catalogs. +The generated code can filter large responses and return only the final value the model needs. Intermediate results stay inside the sandbox instead of entering the model context after every operation. Adding a tool adds a module export, not another tool definition in every prompt, so Code Mode suits large tool catalogs. -Code Mode also supports progressive discovery. The model can search available connectors and request detailed types only for the methods it needs. Successful programs can be saved as reusable snippets. - -For runtime behavior, approvals, replay, and snippets, refer to [How Code Mode works](/agents/tools/codemode/how-it-works/). +To set it up, refer to [Run code with Computer](/agents/runtime/code-mode/computer/). ## Choose where tools run -Execution location describes where a tool implementation runs. It is independent from the model interface. Tools in each location can be exposed directly or through Code Mode. +Execution location describes where a tool implementation runs. It is independent from the model interface. Most tools can be exposed directly or through Code Mode. | Location | Use when | Start here | | ------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- | @@ -57,7 +55,7 @@ Execution location describes where a tool implementation runs. It is independent With direct tool calls, the model calls the tool and the framework routes execution to the configured location. -With Code Mode, generated code runs in a sandbox. Calls from that code cross the sandbox boundary to the underlying tool implementation. Server tools can execute in the host Worker, browser-owned tools can execute in the parent page, and Agent tools can delegate work to another Agent. +With Code Mode, generated code runs in a sandbox. Calls from that code cross the sandbox boundary to host modules, which run in the agent. Server tools and Agent tools can be called this way. Browser tools cannot, because they run in the user's page. ## Connect external tools with MCP @@ -66,9 +64,9 @@ The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) An Agent can expose MCP tools through either model interface: - Pass MCP tools directly to a model with the [Agents MCP client](/agents/tools/mcp/). -- Expose MCP tools inside Code Mode for composition and progressive discovery with [MCP connectors](/agents/tools/codemode/mcp/). +- Let model-written code call MCP tools from every connected server with [`codemode:tools`](/agents/runtime/code-mode/computer/#supply-mcp-tools). -An MCP server can also expose Code Mode itself. For example, it can present one `code` tool or separate `search` and `execute` tools. For these server-side patterns, refer to [Build a Code Mode MCP server](/agents/model-context-protocol/guides/build-codemode-mcp-server/). +An MCP server can also do Code Mode itself, for example by exposing `search` and `execute` tools. Refer to [Code Mode on the server](/agents/concepts/code-mode/#code-mode-on-the-server). ## Control side effects @@ -76,4 +74,4 @@ Approval policy is also independent from execution location and tool source. Any Direct tools can use the standard [human-in-the-loop approval pattern](/agents/concepts/agentic-patterns/human-in-the-loop/). -The durable Code Mode runtime can pause generated code before an annotated connector method executes. Approval replays completed calls from the execution log, applies the approved action, and continues the same program. +With Code Mode, a host module function can pause the run until a person answers. On approval, the run replays completed calls from a journal and continues. Refer to [Ask for approval](/agents/runtime/code-mode/computer/#ask-for-approval). diff --git a/src/content/docs/agents/harnesses/pi/extensions.mdx b/src/content/docs/agents/harnesses/pi/extensions.mdx index c330efe392f..832b96867a4 100644 --- a/src/content/docs/agents/harnesses/pi/extensions.mdx +++ b/src/content/docs/agents/harnesses/pi/extensions.mdx @@ -154,7 +154,7 @@ harness = new PiHarness({ }); ``` -The tools match the ones [Think](/agents/harnesses/think/) offers, so a skill written for one works in the other. The sources are read once, when the factory builds the extension. The model can read a skill's instructions and resources. To run skill scripts, give the model a shell or Workspace tool. Refer to [Execution](/agents/skills/execution/). +The tools match the ones [Think](/agents/harnesses/think/) offers, so a skill written for one works in the other. The sources are read once, when the factory builds the extension. The model can read a skill's instructions and resources. To run skill scripts, give the model a shell or Workspace tool. Refer to [Run skill scripts](/agents/runtime/code-mode/skill-scripts/). ## Use more of Pi Durable diff --git a/src/content/docs/agents/harnesses/think/actions.mdx b/src/content/docs/agents/harnesses/think/actions.mdx index ec0b9fc3ae4..6b689f5e919 100644 --- a/src/content/docs/agents/harnesses/think/actions.mdx +++ b/src/content/docs/agents/harnesses/think/actions.mdx @@ -157,7 +157,7 @@ await agent.rejectExecution(executionId, "Not this release"); -`approveExecution()` runs `execute` once and auto-continues the turn even if no client is connected; `rejectExecution()` resolves the action without running it. `pendingApprovals()` merges parked actions and paused [Codemode](/agents/tools/codemode/) executions, so a single approval UI can drive both. (`durable-pause` requires an `approval` policy — an action that would never park is rejected at definition time.) +`approveExecution()` runs `execute` once and auto-continues the turn even if no client is connected; `rejectExecution()` resolves the action without running it. `pendingApprovals()` merges parked actions and paused executions from the legacy [Code Mode runtime](/agents/runtime/code-mode/legacy/), so a single approval UI can drive both. (`durable-pause` requires an `approval` policy — an action that would never park is rejected at definition time.) Both approval-gated and durable-pause parts carry a stable `ActionApprovalDescriptor` (`{ requestId, toolCallId, action, summary, input, permissions, risk, kind }`) so your UI has everything it needs to render the prompt. diff --git a/src/content/docs/agents/harnesses/think/configuration.mdx b/src/content/docs/agents/harnesses/think/configuration.mdx index 5b167a0bc2c..1cbff15e574 100644 --- a/src/content/docs/agents/harnesses/think/configuration.mdx +++ b/src/content/docs/agents/harnesses/think/configuration.mdx @@ -26,7 +26,7 @@ Think is configured by overriding methods and properties on your `Think` subclas | `sendReasoning` | `true` | Send reasoning chunks to chat clients | | `configureSession()` | identity | Add context blocks, compaction, search, skills — refer to [Sessions](/agents/runtime/capabilities/sessions/) | | `getSkills()` | `[]` | Return Agent Skills sources for on-demand skill activation — refer to [Agent Skills](/agents/skills/) | -| `getSkillScriptRunner()` | `null` | Enable the experimental `run_skill_script` tool — refer to [Execution](/agents/skills/execution/) | +| `getSkillScriptRunner()` | `null` | Enable the experimental `run_skill_script` tool — refer to [Run skill scripts](/agents/runtime/code-mode/skill-scripts/) | | `messageConcurrency` | `"queue"` | How overlapping submits behave — refer to [Client tools](/agents/harnesses/think/client-tools/#message-concurrency) | | `includeMcpTools` | `true` | Convert connected MCP tools to AI SDK tools and add them to model turns. Refer to [MCP tools](/agents/harnesses/think/tools/#mcp-tools) | | `waitForMcpConnections` | `false` | Wait for MCP servers before inference | diff --git a/src/content/docs/agents/harnesses/think/tools.mdx b/src/content/docs/agents/harnesses/think/tools.mdx index 6e06aed2214..235edb4e18e 100644 --- a/src/content/docs/agents/harnesses/think/tools.mdx +++ b/src/content/docs/agents/harnesses/think/tools.mdx @@ -72,7 +72,7 @@ export class MyAgent extends Think { id: "js", loader: this.env.LOADER, access: "read-write", - modules: { "ws:git": createGitModule() }, + modules: { "codemode:git": createGitModule() }, }), ], }); @@ -100,7 +100,7 @@ Computer also ships a `just-bash` shell backend that runs in Dynamic Workers, an ### Call the agent's tools from code -Give a JavaScript backend a `ws:tools` module, and code the model runs with `exec` can import the agent's tools. Code reads files through `node:fs/promises`, so serve `getTools()` rather than the workspace tools: +Give a JavaScript backend a `codemode:tools` module, and code the model runs with `exec` can import the agent's tools. Code reads files through `node:fs/promises`, so serve `getTools()` rather than the workspace tools: @@ -112,7 +112,7 @@ new WorkerJavaScriptBackend({ id: "js", loader: this.env.LOADER, modules: { - "ws:tools": createToolBindings(forAISDKTools(() => this.getTools())), + "codemode:tools": createToolBindings(forAISDKTools(() => this.getTools())), }, }); ``` @@ -123,7 +123,7 @@ Code the model runs with `exec`: ```js import { readFile } from "node:fs/promises"; -import { getWeather } from "ws:tools"; +import { getWeather } from "codemode:tools"; export default async function () { const cities = JSON.parse(await readFile("/workspace/cities.json", "utf8")); @@ -133,7 +133,7 @@ export default async function () { } ``` -`ws:tools` does not export tools that need approval or tools that run in the browser, because nothing can approve or run them from code. Calls from code do not go through `beforeToolCall`. Refer to [`ws:tools`](https://github.com/cloudflare/computer/blob/main/docs/17_isolate_javascript.md#wstools) for how calls and results work. +`codemode:tools` does not export tools that need approval or tools that run in the browser, because nothing can approve or run them from code. Calls from code do not go through `beforeToolCall`. Refer to [`codemode:tools`](https://github.com/cloudflare/computer/blob/main/docs/17_isolate_javascript.md#codemodetools) for how calls and results work. ### Change the tools @@ -270,9 +270,9 @@ export class MyAgent extends Think { -`includeMcpTools` controls only the automatic model tool merge. MCP connections still register, restore, discover, and wait. Raw catalog access, direct calls, Code Mode connectors, and explicit `this.mcp.getAITools()` calls continue to work. +`includeMcpTools` controls only the automatic model tool merge. MCP connections still register, restore, discover, and wait. Raw catalog access, direct calls, and explicit `this.mcp.getAITools()` calls continue to work. -Use this property instead of removing MCP tool names through `activeTools` in `beforeTurn`. Think converts MCP schemas before it calls `beforeTurn`, so `activeTools` cannot avoid that conversion. To configure a connector runtime, refer to [Use MCP tools with Code Mode](/agents/tools/codemode/mcp/). +Use this property instead of removing MCP tool names through `activeTools` in `beforeTurn`. Think converts MCP schemas before it calls `beforeTurn`, so `activeTools` cannot avoid that conversion. To call MCP tools from model-written code, refer to [Supply MCP tools](/agents/runtime/code-mode/computer/#supply-mcp-tools). Add MCP servers programmatically or via `@callable` methods: @@ -303,7 +303,7 @@ export class MyAgent extends Think { ## Code execution tool :::caution[Deprecated] -`createExecuteTool` and `createExecuteRuntime` are deprecated. Use `exec` on a JavaScript backend with a `ws:tools` module instead. Refer to [Call the agent's tools from code](#call-the-agents-tools-from-code). The execute tool keeps working until it is removed along with `@cloudflare/codemode`. +`createExecuteTool` and `createExecuteRuntime` are deprecated. Use `exec` on a JavaScript backend with a `codemode:tools` module instead. Refer to [Call the agent's tools from code](#call-the-agents-tools-from-code). The execute tool is built on the [legacy Code Mode API](/agents/runtime/code-mode/legacy/), which is feature-frozen. It keeps working but gets no new features or fixes. ::: Let the LLM write and run JavaScript in a sandboxed Worker, recorded on a durable Code Mode runtime (abort-and-replay, human approvals, audit trail, reusable snippets). Requires `@cloudflare/codemode` and a `worker_loaders` binding. diff --git a/src/content/docs/agents/index.mdx b/src/content/docs/agents/index.mdx index e8d675f386d..1176267e2dc 100644 --- a/src/content/docs/agents/index.mdx +++ b/src/content/docs/agents/index.mdx @@ -26,7 +26,7 @@ Agents on Cloudflare are composed from four parts: - **The runtime is a Durable Object, the Lifecycle, and capabilities.** Each agent is a [Durable Object](/durable-objects/), and [routing](/agents/runtime/routing/) sends each request to the right one. The Agents SDK [Lifecycle](/agents/runtime/lifecycle/) runs inside the Durable Object, handles its requests, alarms, and WebSocket events, and starts the [capabilities](/agents/runtime/capabilities/) you add to it with `.use()`. Capabilities include [`WebSockets`](/agents/runtime/capabilities/websockets/), [`State`](/agents/runtime/capabilities/state/), [`Scheduler`](/agents/runtime/capabilities/scheduler/), and [`MCPClientManager`](/agents/runtime/capabilities/mcp-client-manager/). - **The harness is the capability that runs the agent loop.** A harness calls models, runs tools, handles tool results, streams responses, and decides whether to continue. It is the unit of compute for an agent, and the Agents SDK exists to make harnesses run well on Cloudflare. Use the [Pi harness](/agents/harnesses/pi/), which runs the [Pi](https://pi.dev/) agent loop durably and keeps a run going after the object is evicted. -- **Tools** let the agent act. For a durable file system and code execution, give the agent a [workspace](/agents/tools/filesystem/) from Cloudflare Computer. The Agents SDK also includes [web fetch](/agents/tools/web-fetch/), [web search](/agents/tools/web-search/), [browser automation](/agents/tools/browser/), [sandboxed code execution](/agents/tools/sandbox/), [AI Search](/agents/tools/ai-search/), [MCP servers](/agents/tools/mcp/), [payments](/agents/tools/payments/), and [Code Mode](/agents/tools/codemode/). +- **Tools** let the agent act. For a durable file system and code execution, give the agent a [workspace](/agents/tools/filesystem/) from Cloudflare Computer. The Agents SDK also includes [web fetch](/agents/tools/web-fetch/), [web search](/agents/tools/web-search/), [browser automation](/agents/tools/browser/), [sandboxed code execution](/agents/tools/sandbox/), [AI Search](/agents/tools/ai-search/), [MCP servers](/agents/tools/mcp/), and [payments](/agents/tools/payments/). With [Code Mode](/agents/concepts/code-mode/), the model writes code that calls these tools instead of calling them one at a time. - **Communication channels** define how users and systems reach your agent, such as [chat](/agents/agent-class/communication-channels/chat/), [voice](/agents/agent-class/communication-channels/voice/), [email](/agents/agent-class/communication-channels/email/), [Slack](/agents/agent-class/communication-channels/slack/), [webhooks](/agents/agent-class/communication-channels/webhooks/), and other event sources. These guides use the [`Agent` class](/agents/agent-class/), a preset Durable Object with the Lifecycle and a set of capabilities already installed. Agents publish structured events, such as state changes, schedules, and MCP connections, that you can use for [observability](/agents/runtime/observability/). diff --git a/src/content/docs/agents/model-context-protocol/cloudflare/servers-for-cloudflare/index.mdx b/src/content/docs/agents/model-context-protocol/cloudflare/servers-for-cloudflare/index.mdx index 7051d6ddf42..85e75b1a970 100644 --- a/src/content/docs/agents/model-context-protocol/cloudflare/servers-for-cloudflare/index.mdx +++ b/src/content/docs/agents/model-context-protocol/cloudflare/servers-for-cloudflare/index.mdx @@ -22,7 +22,7 @@ The [Cloudflare API MCP server](https://github.com/cloudflare/mcp) at `https://m The server provides the `search()` and `execute()` tools to find and call API endpoints, and the `docs()` tool to search Cloudflare developer documentation. -It uses [the search-and-execute Code Mode pattern](/agents/model-context-protocol/guides/build-codemode-mcp-server/#publish-a-large-api-with-search-and-execute), a technique where the model writes JavaScript against a typed representation of the OpenAPI spec and the Cloudflare API client, rather than loading individual tool definitions for each endpoint. The generated code runs inside an isolated [Dynamic Worker](/dynamic-workers/) sandbox. +It uses [the search-and-execute Code Mode pattern](/agents/concepts/code-mode/#code-mode-on-the-server), a technique where the model writes JavaScript against a typed representation of the OpenAPI spec and the Cloudflare API client, rather than loading individual tool definitions for each endpoint. The generated code runs inside an isolated [Dynamic Worker](/dynamic-workers/) sandbox. This approach uses approximately 1,000 tokens regardless of how many API endpoints exist. An equivalent MCP server that exposed every endpoint as a native tool would consume over 1 million tokens — more than the entire context window of most foundation models. diff --git a/src/content/docs/agents/model-context-protocol/guides/build-codemode-mcp-server.mdx b/src/content/docs/agents/model-context-protocol/guides/build-codemode-mcp-server.mdx deleted file mode 100644 index eadd4b5df25..00000000000 --- a/src/content/docs/agents/model-context-protocol/guides/build-codemode-mcp-server.mdx +++ /dev/null @@ -1,372 +0,0 @@ ---- -title: Build a Code Mode MCP server -description: Expose an MCP server or a large OpenAPI service to any MCP client through sandboxed, model-written code. -pcx_content_type: how-to -sidebar: - order: 14 -tags: - - AI - - MCP -products: - - agents ---- - -import { - PackageManagers, - Steps, - TypeScriptExample, - WranglerConfig, -} from "~/components"; - -A Code Mode MCP server lets any Model Context Protocol (MCP) client use model-written code without providing its own sandbox. Instead of advertising one tool per operation, the server exposes code execution as its tool interface and runs the generated JavaScript in an isolated Worker. Generated code can compose several operations and return only the final result, so tool definitions and intermediate data stay out of the model's context. For the design rationale, refer to [Code Mode: give agents an entire API in 1,000 tokens](https://blog.cloudflare.com/code-mode-mcp/). - -:::caution -Code Mode is experimental and may have breaking changes. Use caution in production. -::: - -## Choose a pattern - -| Pattern | MCP tools | Model-facing API | Use when | -| ------------------ | ------------------- | ------------------------------------- | ---------------------------------------------------------------------- | -| Single code tool | `code` | Typed methods for every upstream tool | You already have an MCP server with a manageable set of tools. | -| Search and execute | `search`, `execute` | OpenAPI document and request function | You have a large API whose complete schema should stay out of context. | - -Use `codeMcpServer()` when an existing MCP server already defines the operations and schemas the model needs. Use `openApiMcpServer()` when a large OpenAPI catalog needs progressive discovery and a fixed model-context footprint. - -Both functions currently return an SDK v1 server. Serve it with `createLegacyMcpHandler`, as shown in [Wrap an existing MCP server](#wrap-an-existing-mcp-server) and [Publish a large API with search and execute](#publish-a-large-api-with-search-and-execute). - -## Wrap an existing MCP server - -The single-tool pattern wraps an existing MCP server with `codeMcpServer()`. Instead of advertising each upstream tool separately, the server advertises one `code` tool. - -The `code` tool description contains generated TypeScript definitions for every upstream tool. The model writes JavaScript against a `codemode` namespace: - -```js -async () => { - const projects = await codemode.list_projects({ status: "active" }); - const tasks = []; - - for (const project of projects) { - tasks.push(...(await codemode.list_tasks({ projectId: project.id }))); - } - - return tasks.filter((task) => task.status === "blocked"); -}; -``` - -The MCP client makes one outer tool call. Inside the sandbox, the code can make dependent upstream calls, filter intermediate data, and return only the final result. - -This pattern works well when the generated type declarations fit comfortably in the `code` tool description. The model receives those declarations when it loads the MCP tool. - -You need a Cloudflare Workers project and an existing `McpServer`. - - - -1. Install Code Mode and the MCP dependencies: - - - -2. Add a Worker Loader binding and the `nodejs_compat` compatibility flag: - - - - ```toml - name = "codemode-mcp-server" - main = "src/server.ts" - upload_source_maps = true - compatibility_date = "$today" - compatibility_flags = ["nodejs_compat"] - - [[worker_loaders]] - binding = "LOADER" - ``` - - - -3. Create the upstream server and pass it to `codeMcpServer()`: - - - - ```ts - import { DynamicWorkerExecutor } from "@cloudflare/codemode"; - import { codeMcpServer } from "@cloudflare/codemode/mcp"; - import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; - import { createLegacyMcpHandler } from "agents/mcp"; - import { z } from "zod"; - - function createOrderServer() { - const server = new McpServer({ - name: "orders", - version: "1.0.0", - }); - - server.registerTool( - "get_order", - { - description: "Get an order by ID", - inputSchema: { - orderId: z.string().describe("Order ID"), - }, - }, - async ({ orderId }) => ({ - structuredContent: { - id: orderId, - status: "processing", - }, - content: [ - { - type: "text", - text: JSON.stringify({ id: orderId, status: "processing" }), - }, - ], - }), - ); - - return server; - } - - export default { - async fetch(request, env, ctx): Promise { - const upstream = createOrderServer(); - const executor = new DynamicWorkerExecutor({ loader: env.LOADER }); - const server = await codeMcpServer({ - server: upstream, - executor, - }); - - return createLegacyMcpHandler(server, { route: "/mcp" })( - request, - env, - ctx, - ); - }, - } satisfies ExportedHandler; - ``` - - - -4. Deploy the Worker: - - - -5. In an MCP client, connect to `https://..workers.dev/mcp`. Verify that the server exposes one tool named `code`. - - - -The model can use the generated `codemode` namespace inside the `code` tool: - -```js -async () => { - const order = await codemode.get_order({ orderId: "order-123" }); - return { id: order.id, status: order.status }; -}; -``` - -When an upstream tool returns `structuredContent`, Code Mode exposes that value directly. Text-only content is joined and parsed as JSON when possible. Upstream MCP errors become exceptions that model-written code can catch. Mixed text and binary content remains in its MCP result structure. - -If you provide a custom `description`, use `{{types}}` where the generated TypeScript declarations should appear. Use `{{example}}` where the SDK should insert an example call based on the first upstream MCP tool. Both placeholders are optional. - -### Protect upstream operations - -`codeMcpServer()` does not provide durable approval for each upstream tool call. It invokes upstream handlers from inside the outer `code` tool. - -Enforce authorization and any required per-operation approval in each upstream handler before applying side effects. Do not include credentials in tool results. - -## Publish a large API with search and execute - -A large API can have thousands of operations. Including every operation in one tool description would still consume substantial context. The search-and-execute pattern separates capability discovery from authenticated API calls. - -The server exposes two MCP tools: - -- `search` runs generated code against an OpenAPI document. It returns only the operations, parameters, or schemas needed for the task. -- `execute` runs generated code with an authenticated request function. It can call the selected operations, compose responses, and return a focused result. - -The complete OpenAPI document stays inside the sandbox. Only what the search code returns enters the model context. - -Authentication stays in the host request callback. The generated code receives a request function, not the credential. - -The [Cloudflare API MCP server](/agents/model-context-protocol/cloudflare/servers-for-cloudflare/) uses this pattern to expose the Cloudflare API through `search` and `execute`. For the design rationale and context savings, refer to [Code Mode: give agents an entire API in 1,000 tokens](https://blog.cloudflare.com/code-mode-mcp/). - -You need a Cloudflare Workers project, an OpenAPI 3.x document, and a host-side method for authenticating API requests. - - - -1. Install Code Mode and the MCP dependencies: - - - -2. Add a Worker Loader binding and the `nodejs_compat` compatibility flag: - - - - ```toml - name = "openapi-codemode-mcp" - main = "src/server.ts" - upload_source_maps = true - compatibility_date = "$today" - compatibility_flags = ["nodejs_compat"] - - [[worker_loaders]] - binding = "LOADER" - ``` - - - -3. Load the OpenAPI document on the host. Create the MCP server with an authenticated `request` function: - - - - ```ts - import { DynamicWorkerExecutor } from "@cloudflare/codemode"; - import { openApiMcpServer } from "@cloudflare/codemode/mcp"; - import { createLegacyMcpHandler } from "agents/mcp"; - - const SPEC_URL = "https://api.example.com/openapi.json"; - const API_ORIGIN = "https://api.example.com"; - - let specCache: Record | undefined; - - async function loadSpec(): Promise> { - if (specCache) return specCache; - - const response = await fetch(SPEC_URL); - if (!response.ok) { - throw new Error(`OpenAPI request failed: ${response.status}`); - } - - specCache = (await response.json()) as Record; - return specCache; - } - - export default { - async fetch(request, env, ctx): Promise { - const authorization = request.headers.get("Authorization"); - if (!authorization?.startsWith("Bearer ")) { - return new Response("Bearer token required", { status: 401 }); - } - - const server = openApiMcpServer({ - spec: await loadSpec(), - executor: new DynamicWorkerExecutor({ loader: env.LOADER }), - name: "example-api", - version: "1.0.0", - request: async (options) => { - if (!options.path.startsWith("/")) { - throw new Error("API path must start with a slash"); - } - - const url = new URL(`${API_ORIGIN}${options.path}`); - for (const [key, value] of Object.entries(options.query ?? {})) { - if (value !== undefined) { - url.searchParams.set(key, String(value)); - } - } - - const headers: Record = { Authorization: authorization }; - if (options.contentType) { - headers["Content-Type"] = options.contentType; - } else if (options.body !== undefined) { - headers["Content-Type"] = "application/json"; - } - - const response = await fetch(url, { - method: options.method, - headers, - body: - options.body === undefined - ? undefined - : options.rawBody - ? (options.body as string) - : JSON.stringify(options.body), - }); - - if (!response.ok) { - throw new Error(`API request failed: ${response.status}`); - } - if (response.status === 204) return null; - - const responseType = response.headers.get("Content-Type") ?? ""; - return responseType.includes("application/json") - ? await response.json() - : await response.text(); - }, - }); - - return createLegacyMcpHandler(server, { route: "/mcp" })( - request, - env, - ctx, - ); - }, - } satisfies ExportedHandler; - ``` - - - -4. Deploy the Worker: - - - -5. In an MCP client, connect to `https://..workers.dev/mcp`. Include the bearer token required by your Worker. - -6. List the MCP tools. Verify that the server exposes `search` and `execute`. - - - -### Search the OpenAPI document - -Call `search` before `execute`. Search code can inspect the document without making API requests: - -```js -async () => { - const spec = await codemode.spec(); - return Object.entries(spec.paths) - .filter(([path]) => path.includes("/orders")) - .map(([path, operations]) => ({ - path, - methods: Object.keys(operations), - })); -}; -``` - -Local OpenAPI `$ref` values resolve inside the sandbox when code calls `codemode.spec()`. External references remain unresolved. - -### Call the API - -The `execute` tool includes the same `codemode.spec()` method and the host-provided `codemode.request()` method: - -```js -async () => { - const response = await codemode.request({ - method: "GET", - path: "/orders", - query: { status: "processing", limit: 20 }, - }); - - return response.items.map(({ id, status }) => ({ id, status })); -}; -``` - -The host callback receives `method`, `path`, optional `query`, optional `body`, optional `contentType`, and optional `rawBody` fields. For exact types, refer to the [`openApiMcpServer()` API](/agents/tools/codemode/api-reference/#openapimcpserver). - -The `search` and `execute` tools use fixed example snippets. An optional `description` is appended to the `execute` tool description. This function does not use the `{{types}}` or `{{example}}` placeholders supported by [`codeMcpServer()`](#wrap-an-existing-mcp-server). - -### Protect the API - -The example reads the bearer token before creating the MCP server. Its request callback adds that token to outbound requests. The token never enters the sandbox. - -`openApiMcpServer()` does not provide durable approval for each request inside `execute`. Enforce authorization and any required per-operation approval in the host callback before applying side effects. Validate paths instead of accepting arbitrary origins. - -Do not include secrets in the OpenAPI document or API results. Both are available to model-written code. - -## Sandbox and authorization boundary - -Model-written code runs in an isolated Worker. `DynamicWorkerExecutor` blocks direct outbound `fetch()` and `connect()` calls by default, so generated code reaches external systems only through upstream MCP tools or the host-provided request callback. - -Code execution does not replace authorization. Enforce permissions and any required approval inside upstream tool handlers or the host request callback before applying side effects. Do not expose credentials through tool results or OpenAPI documents. - -## Limit results - -Have model-written code select, map, aggregate, or paginate data before returning. The publisher limits final MCP responses to approximately 6,000 estimated tokens and marks truncated responses with `--- TRUNCATED ---`. - -Truncation does not reduce API work already performed. Return focused identifiers, status fields, counts, and errors that support the model's next decision. diff --git a/src/content/docs/agents/model-context-protocol/guides/migrate-to-mcp-sdk-v2.mdx b/src/content/docs/agents/model-context-protocol/guides/migrate-to-mcp-sdk-v2.mdx index b5dda1ab2f7..e635e986b0a 100644 --- a/src/content/docs/agents/model-context-protocol/guides/migrate-to-mcp-sdk-v2.mdx +++ b/src/content/docs/agents/model-context-protocol/guides/migrate-to-mcp-sdk-v2.mdx @@ -362,7 +362,7 @@ These integrations accept an SDK v2 `McpServer`. Serve them with `createMcpHandl These integrations still produce or require SDK v1 servers: -- The Code Mode `codeMcpServer` and `openApiMcpServer` helpers. Refer to [Build a Code Mode MCP server](/agents/model-context-protocol/guides/build-codemode-mcp-server/). +- The `codeMcpServer` and `openApiMcpServer` helpers from the [legacy Code Mode API](/agents/runtime/code-mode/legacy/). - The `mppx` `Transport.mcpSdk()` payment transport. Refer to [Accept payments](/agents/tools/payments/mpp/accept-payments/). Serve their output with `createLegacyMcpHandler`, building a new server for each request, until they produce SDK v2 servers. The Code Mode MCP connector and `withX402Client` accept either client generation. diff --git a/src/content/docs/agents/model-context-protocol/index.mdx b/src/content/docs/agents/model-context-protocol/index.mdx index 9ac8a988e07..2ca38b4b128 100644 --- a/src/content/docs/agents/model-context-protocol/index.mdx +++ b/src/content/docs/agents/model-context-protocol/index.mdx @@ -31,7 +31,7 @@ The MCP standard supports two modes of operation: ### Best practices -- **Keep tool definitions out of the context window**: MCP clients used to send every tool definition straight to the model, so a server with many tools filled the agent's context window. Newer clients use Code Mode instead: the model writes code that calls your tools and loads only the definitions it needs. You can apply the same pattern on the server, exposing a large API through a small search-and-execute interface. Refer to [Code Mode for MCP servers](https://blog.cloudflare.com/code-mode-mcp/) and [Build a Code Mode MCP server](/agents/model-context-protocol/guides/build-codemode-mcp-server/). +- **Keep tool definitions out of the context window**: MCP clients used to send every tool definition straight to the model, so a server with many tools filled the agent's context window. Newer clients use Code Mode instead: the model writes code that calls your tools and loads only the definitions it needs. You can apply the same pattern on the server, exposing a large API through a small search-and-execute interface. Refer to [Code Mode](/agents/concepts/code-mode/) and [Code Mode for MCP servers](https://blog.cloudflare.com/code-mode-mcp/). - **Use granular authorization**: Protect your server with OAuth, and give each operation its own scopes. When a token lacks a scope, return an `insufficientScope()` challenge so the client asks the user to approve it (step-up authorization) instead of requesting every scope up front. Refer to [Authorization](/agents/model-context-protocol/protocol/authorization/). - **Declare accurate input and output schemas**: Strict MCP clients validate tool calls against `inputSchema` and tool results against `outputSchema`, and reject tools whose results do not match. Define both schemas for every tool, and return `structuredContent` that matches `outputSchema`. Refer to [Tools](/agents/model-context-protocol/protocol/tools/). diff --git a/src/content/docs/agents/runtime/code-mode/computer.mdx b/src/content/docs/agents/runtime/code-mode/computer.mdx new file mode 100644 index 00000000000..abfc99ce27b --- /dev/null +++ b/src/content/docs/agents/runtime/code-mode/computer.mdx @@ -0,0 +1,306 @@ +--- +title: Run code with Computer +description: Give the model an exec tool with Cloudflare Computer, and supply your functions, the agent's tools, MCP servers, and APIs to its code as codemode:* modules. +pcx_content_type: how-to +sidebar: + order: 1 +products: + - agents +--- + +import { + PackageManagers, + TypeScriptExample, + WranglerConfig, +} from "~/components"; + +[Cloudflare Computer](https://github.com/cloudflare/computer) is the recommended way to add [Code Mode](/agents/concepts/code-mode/) to an agent. It gives the model an `exec` tool that runs JavaScript in a [Dynamic Worker](/dynamic-workers/). You decide what that code can reach by passing it modules. Each module is imported under a `codemode:` name, and its functions run in your agent. + +Computer runs code with `WorkerJavaScriptBackend` from [`@cloudflare/codemode`](/agents/runtime/code-mode/executor/), and adds a persistent file system, the model-facing tools, and prebuilt modules. For the file system and file tools, refer to [File system](/agents/tools/filesystem/). + +## Install + + + +The AI SDK tools also need `ai`. Your Worker needs the `nodejs_compat` [compatibility flag](/workers/configuration/compatibility-flags/) and a [Worker Loader binding](/dynamic-workers/): + + + +```jsonc +{ + "compatibility_date": "$today", + "compatibility_flags": ["nodejs_compat"], + "worker_loaders": [{ "binding": "LOADER" }], +} +``` + + + +## Give the model an exec tool + +Create a `Workspace` with a `WorkerJavaScriptBackend`, and pass its tools to the model. Once the workspace has a backend, the tools include `exec`: + + + +```ts +import { DurableObject } from "cloudflare:workers"; +import { Workspace } from "@cloudflare/computer"; +import { WorkerJavaScriptBackend } from "@cloudflare/computer/backends/worker-javascript"; +import { createAITools } from "@cloudflare/computer/tools/ai-sdk"; +import { Lifecycle } from "agents/lifecycle"; +import { createAI } from "agents/models/ai-sdk"; +import { generateText, stepCountIs } from "ai"; + +export class OrdersAgent extends DurableObject { + readonly lifecycle = Lifecycle.install(this); + + workspace = new Workspace({ + storage: this.ctx.storage, + backends: [ + new WorkerJavaScriptBackend({ + loader: this.env.LOADER, + modules: { + "codemode:orders": { + lookup: ([orderId]) => this.lookUpOrder(String(orderId)), + }, + }, + }), + ], + }); + + async onRequest(request: Request): Promise { + const { text } = await generateText({ + model: createAI({ binding: this.env.AI })("@cf/moonshotai/kimi-k2.6"), + prompt: await request.text(), + tools: createAITools({ workspace: this.workspace }), + stopWhen: stepCountIs(10), + }); + return new Response(text); + } + + async lookUpOrder(orderId: string) { + const row = this.ctx.storage.sql + .exec("SELECT id, status, total FROM orders WHERE id = ?", orderId) + .toArray()[0]; + return row ?? null; + } +} +``` + + + +The `exec` tool describes the backend to the model, including the modules its code can import. The model can then write code like this without more instructions: + +```js +import { lookup } from "codemode:orders"; + +export default async function () { + const ids = ["A-100", "A-101", "A-102"]; + const orders = await Promise.all(ids.map((id) => lookup(id))); + return orders.filter((order) => order?.status === "late"); +} +``` + +The module must have a default export. Its return value is the result of the run, and it must be JSON-compatible. Call `node:fs` and modules inside the default export, not at the top level. The Workers runtime allows no I/O while a module loads. + +## Supply your own functions + +A host module is an object of functions under a `codemode:` name. Each function receives the arguments from the code as an array, and a context object: + + + +```ts +new WorkerJavaScriptBackend({ + loader: this.env.LOADER, + modules: { + "codemode:billing": { + refund: ([orderId], { callId }) => + this.refund(String(orderId), { idempotencyKey: callId }), + }, + }, +}); +``` + + + +The context includes `callId`, a stable ID you can pass on as an idempotency key, and `signal`, which aborts when the run ends. Arguments and return values must be JSON-compatible plain data. + +The arguments come from model-written code, so validate them as you would any user input. A host module runs with the full access of your agent. Expose only the functions the code needs. + +## Supply the agent's tools + +`codemode:tools` exposes tools that the agent already has, each under its own name. Build it with `createToolBindings()`, and an adapter for your tool format: + + + +```ts +import { createToolBindings } from "@cloudflare/computer/modules/tools"; +import { forAISDKTools } from "@cloudflare/computer/tools/ai-sdk"; +import { myTools } from "./tools"; // An AI SDK ToolSet + +new WorkerJavaScriptBackend({ + loader: this.env.LOADER, + modules: { + "codemode:tools": createToolBindings(forAISDKTools(() => myTools)), + }, +}); +``` + + + +Each export takes the same argument object the model would pass, and resolves to `{ text, details, isError }`: + +```js +import { getWeather } from "codemode:tools"; + +export default async function ({ cities }) { + const reports = []; + for (const city of cities) { + const { text, isError } = await getWeather({ city }); + if (!isError) reports.push(text); + } + return reports; +} +``` + +`forAISDKTools()` checks arguments against each tool's `inputSchema`, as the AI SDK checks a model's call. For [Pi](/agents/harnesses/pi/) tools, use `forPiTools()` from `@cloudflare/computer/tools/pi-ai`. Pass a function of tools, not a fixed list, so a call always reaches the agent's current tools. + +`codemode:tools` leaves out tools that need approval and tools that run in the browser, because nothing can approve or run them from code. It also leaves out `exec` itself. + +## Supply MCP tools + +Tools from MCP servers reach the code the same way. Connect servers with the [`MCPClientManager`](/agents/runtime/capabilities/mcp-client-manager/) capability, and pass its tools to `forAISDKTools()`: + + + +```ts +import { DurableObject } from "cloudflare:workers"; +import { Workspace } from "@cloudflare/computer"; +import { WorkerJavaScriptBackend } from "@cloudflare/computer/backends/worker-javascript"; +import { createToolBindings } from "@cloudflare/computer/modules/tools"; +import { forAISDKTools } from "@cloudflare/computer/tools/ai-sdk"; +import { Lifecycle } from "agents/lifecycle"; +import { MCPClientManager } from "agents/mcp/client"; + +export class ResearchAgent extends DurableObject { + readonly mcp = new MCPClientManager("research-agent", "1.0.0"); + readonly lifecycle = Lifecycle.install(this).use(this.mcp); + + workspace = new Workspace({ + storage: this.ctx.storage, + backends: [ + new WorkerJavaScriptBackend({ + loader: this.env.LOADER, + modules: { + "codemode:tools": createToolBindings( + forAISDKTools(() => this.mcp.getAITools()), + ), + }, + }), + ], + }); +} +``` + + + +One program can now call tools from every connected server, and return only what the model needs. To combine MCP tools with your own, return both from the function: `forAISDKTools(() => ({ ...myTools, ...this.mcp.getAITools() }))`. + +## Supply an API + +To let code call an HTTP API, write a host module that holds the credentials and makes the request. The code never sees the token: + + + +```ts +new WorkerJavaScriptBackend({ + loader: this.env.LOADER, + modules: { + "codemode:github": { + get: async ([path]) => { + if (typeof path !== "string" || !path.startsWith("/repos/")) { + throw new Error("Only /repos/ paths are allowed."); + } + const response = await fetch(`https://api.github.com${path}`, { + headers: { + Authorization: `Bearer ${this.env.GITHUB_TOKEN}`, + "User-Agent": "orders-agent", + }, + }); + return response.json(); + }, + }, + }, +}); +``` + + + +Code that runs in the Dynamic Worker has no network access by default. To let it call the network directly, pass a `Fetcher` as `globalOutbound`, which receives every request the code makes. + +## Prebuilt modules + +Computer includes these modules. Pass them in `modules`: + +| Module | Function | Entry point | What code can do | +| -------------------- | ------------------------- | ---------------------------------------- | --------------------------------------------------------------------- | +| `codemode:tools` | `createToolBindings()` | `@cloudflare/computer/modules/tools` | Call the agent's tools. | +| `codemode:git` | `createGitModule()` | `@cloudflare/computer/modules/git` | Run Git operations on the workspace. Requires the `git` workspace option. | +| `codemode:artifacts` | `createArtifactsModule()` | `@cloudflare/computer/modules/artifacts` | Use [Artifacts](/artifacts/) repositories. | +| `codemode:container` | `createContainerModule()` | `@cloudflare/computer/modules/container` | Run shell commands in the workspace's container backend. | + +## Call a container from code + +Some work needs a shell. Add a container backend to the workspace next to the JavaScript backend, and give the JavaScript backend the `codemode:container` module. The model keeps one `exec` tool, and its code can run commands when it needs to: + +```js +import { exec } from "codemode:container"; + +export default async function () { + const { exitCode, stdout, stderr } = await exec("npm test", { + cwd: "/workspace/app", + }); + return { passed: exitCode === 0, stdout, stderr }; +} +``` + +The container shares the workspace files. For the setup, refer to [`codemode:container`](https://github.com/cloudflare/computer/blob/main/docs/17_isolate_javascript.md#codemodecontainer) in the Computer documentation. + +## Ask for approval + +A host module function can pause a run until a person answers, for example to approve a payment. The code calls the function as usual, and gets the result once the answer arrives. This needs `onInterrupt: "replay"`, so the run can resume from a journal: + + + +```ts +new WorkerJavaScriptBackend({ + loader: this.env.LOADER, + onInterrupt: "replay", + modules: { + "codemode:billing": { + refund: async ([orderId], ctx) => { + const { approved } = await ctx.interrupt<{ approved: boolean }>({ + orderId, + }); + if (!approved) throw new Error("The refund was rejected."); + return this.refund(String(orderId), { idempotencyKey: ctx.callId }); + }, + }, + }, +}); +``` + + + +List waiting calls with `workspace.runtime.pending()`, and answer one with `workspace.runtime.respond()`. For the details and current limits, refer to [Wait for an answer](/agents/tools/filesystem/#wait-for-an-answer). + +## Use Computer with a harness + +- [Think](/agents/harnesses/think/) has a workspace at `this.workspace` and adds `exec` once the workspace has a backend. Refer to [Call the agent's tools from code](/agents/harnesses/think/tools/#call-the-agents-tools-from-code). +- [Pi](/agents/harnesses/pi/) takes the workspace tools from `createPiTools()`. Build `codemode:tools` with `forPiTools()`. Refer to [File system](/agents/tools/filesystem/#pi). + +## Related resources + +- [File system](/agents/tools/filesystem/): the workspace, file tools, backend options, and resuming runs after a restart. +- [Use the executor](/agents/runtime/code-mode/executor/): run code without a workspace. +- [Computer documentation](https://github.com/cloudflare/computer/tree/main/docs): the full interfaces of every module. diff --git a/src/content/docs/agents/runtime/code-mode/executor.mdx b/src/content/docs/agents/runtime/code-mode/executor.mdx new file mode 100644 index 00000000000..1fe33a4c2e6 --- /dev/null +++ b/src/content/docs/agents/runtime/code-mode/executor.mdx @@ -0,0 +1,192 @@ +--- +title: Use the executor +description: Run model-written JavaScript in a Dynamic Worker with WorkerJavaScriptBackend from @cloudflare/codemode, against your own storage and host modules. +pcx_content_type: how-to +sidebar: + order: 2 +products: + - agents +--- + +import { PackageManagers, TypeScriptExample, WranglerConfig } from "~/components"; + +`WorkerJavaScriptBackend` from `@cloudflare/codemode` is the executor behind [Code Mode](/agents/concepts/code-mode/). It runs one ECMAScript module in a fresh [Dynamic Worker](/dynamic-workers/). The code reads files through `node:fs` and imports host modules, whose functions run in your Durable Object. + +[Computer](/agents/runtime/code-mode/computer/) runs its code on this executor and connects it for you. Use the executor directly when you have your own file storage or tool layer, or when you are building your own code tool. You give up the workspace file system, the model-facing `exec` tool, and the prebuilt `codemode:*` modules. + +## Install + + + +Add a [Worker Loader binding](/dynamic-workers/): + + + +```jsonc +{ + "compatibility_date": "$today", + "compatibility_flags": ["nodejs_compat"], + "worker_loaders": [{ "binding": "LOADER" }], +} +``` + + + +## Connect the executor + +The executor runs nothing until it connects. `connect()` takes two things from your Durable Object: + +- `db` is SQLite storage for execution records and their output. The executor creates tables named `workspace_runtime_*`. +- `fs` is the file system that code sees through `node:fs`, as a `WorkspaceRuntimeFilesystem`. The package does not include one. Implement the interface over your own storage, or use a Computer `Workspace`, which has a SQLite-backed file system. + + + +```ts +import { DurableObject } from "cloudflare:workers"; +import { + WorkerJavaScriptBackend, + type WorkspaceModuleBackendHandle, + type WorkspaceRuntimeDatabase, + type WorkspaceRuntimeFilesystem, +} from "@cloudflare/codemode"; +import { Lifecycle } from "agents/lifecycle"; +import { lookUpForecast } from "./weather"; +import { files } from "./files"; // Your WorkspaceRuntimeFilesystem + +export class Runner extends DurableObject { + readonly lifecycle = Lifecycle.install(this); + #executor?: WorkspaceModuleBackendHandle; + + async onStart() { + this.#executor = await new WorkerJavaScriptBackend({ + loader: this.env.LOADER, + modules: { + "codemode:weather": { + forecast: ([city]) => lookUpForecast(String(city)), + }, + }, + }).connect({ db: sqlDatabase(this.ctx.storage), fs: files }); + } + + async onRequest(request: Request): Promise { + const execution = await this.#executor!.exec({ + source: await request.text(), + input: { city: "Lisbon" }, + }); + + let stdout = ""; + let result: unknown; + for await (const event of execution.events) { + if (event.name === "stdout") stdout += new TextDecoder().decode(event.value); + if (event.name === "exit") result = event.result; + } + return Response.json({ result, stdout }); + } +} + +// ctx.storage.sql returns BLOB columns as ArrayBuffer. The executor expects Uint8Array. +function sqlDatabase(storage: DurableObjectStorage): WorkspaceRuntimeDatabase { + function all(query: string, ...bindings: unknown[]): Row[] { + return storage.sql + .exec(query, ...bindings) + .toArray() + .map((row) => { + const out: Record = {}; + for (const [key, value] of Object.entries(row)) { + out[key] = value instanceof ArrayBuffer ? new Uint8Array(value) : value; + } + return out as Row; + }); + } + return { + run: (query, ...bindings) => void storage.sql.exec(query, ...bindings), + all, + one: (query, ...bindings) => all(query, ...bindings)[0], + transactionSync: (closure) => storage.transactionSync(closure), + }; +} +``` + + + +Connect once per Durable Object, and reuse the handle. Connecting marks any execution that was still running as failed, because its Dynamic Worker is gone. + +## What the code looks like + +The source is an ECMAScript module. Its default export is called with `input`, and its return value is the result: + +```js +import { readFile } from "node:fs/promises"; +import { forecast } from "codemode:weather"; + +export default async function ({ city }) { + const notes = await readFile("/workspace/notes.txt", "utf8"); + return { notes, forecast: await forecast(city) }; +} +``` + +Call `node:fs` and host modules inside the default export. The Workers runtime allows no I/O while a module loads, so a call at the top level fails the run. The result and host module arguments must be JSON-compatible plain data. + +Every path is confined to `root`, which is `/workspace` by default. + +## Events + +`exec()` returns an execution with an `events` stream: + +| Event | Fields | +| -------- | ---------------------------------------------------------------- | +| `stdout` | `value`, the bytes the code wrote with `console.log()` | +| `stderr` | `value`, the bytes the code wrote with `console.error()` | +| `exit` | `code`, the exit code, and `result`, the default export's return | + +Finished executions stay in SQLite for 60 minutes, up to 100 of them. Call `getExec({ id })` to read an execution's events again, `killExec({ id })` to stop one, and `disposeExec({ id })` to remove it. + +## Modules + +The `modules` option maps a specifier to one of three things: + +- **A string of JavaScript.** It is bundled into the isolate, for example `"tar-stream": TAR_STREAM_BUNDLE`. +- **An object of functions.** This is a host module, under a `codemode:` name. Each function runs in your Durable Object. It receives the code's arguments as an array, and a context with `signal`, `deadline`, `access`, and `resolvePath`. +- **A factory.** It builds the functions when the executor connects. It receives every field you passed to `connect()` except `db` and `fs`, so a module can use services that only exist at that time. + + + +```ts +const backend = new WorkerJavaScriptBackend({ + loader: env.LOADER, + modules: { + "codemode:greeter": ({ greeting }: { greeting: string }) => ({ + greet: ([name]) => `${greeting}, ${String(name)}`, + }), + }, +}); + +await backend.connect({ db, fs, greeting: "Hello" }); +``` + + + +Computer's prebuilt modules, such as `codemode:git` and `codemode:tools`, are factories like this. A Computer `Workspace` passes its Git client and runtime when it connects. + +To describe the modules to a model, read `backend.description`. It lists every module, including each factory's description. + +## Options + +| Option | Default | Description | +| ------------------ | --------------- | ----------------------------------------------------------------------------------- | +| `loader` | Required | The Worker Loader binding. | +| `root` | `/workspace` | The directory code can access. | +| `access` | `"read-write"` | Set to `"read"` to stop code from changing files. | +| `modules` | None | Modules code can import. | +| `defaultTimeoutMs` | `60000` | How long a run can take when the caller does not set a timeout. | +| `maxTimeoutMs` | `180000` | The longest timeout a caller can request. | +| `maxCpuMs` | `maxTimeoutMs` | The most CPU time one run can use. | +| `globalOutbound` | `null` | A `Fetcher` that receives every outbound request. `null` blocks network access. | + +The executor also limits the size of source, input, output, results, and host calls. Every option is documented on `WorkerJavaScriptBackendOptions`. + +## Related resources + +- [Run code with Computer](/agents/runtime/code-mode/computer/): the same executor, with a workspace and an `exec` tool. +- [`@cloudflare/codemode` on GitHub](https://github.com/cloudflare/agents/tree/main/packages/codemode): source and documentation. +- [Dynamic Workers](/dynamic-workers/): the isolates that run each execution. diff --git a/src/content/docs/agents/runtime/code-mode/index.mdx b/src/content/docs/agents/runtime/code-mode/index.mdx new file mode 100644 index 00000000000..2ff5f45a5e8 --- /dev/null +++ b/src/content/docs/agents/runtime/code-mode/index.mdx @@ -0,0 +1,99 @@ +--- +title: Code Mode +description: Run code that a model writes. Use Cloudflare Computer, the executor from @cloudflare/codemode, a container, or skill scripts. +pcx_content_type: overview +sidebar: + order: 5 + group: + hideIndex: false +products: + - agents +--- + +import { CardGrid, LinkCard } from "~/components"; + +Any code execution in an agent is Code Mode. The model writes a program, the agent runs it in a sandbox, and the result goes back to the model. For why this works better than calling tools one at a time, refer to the [Code Mode concept](/agents/concepts/code-mode/). + +This section covers the ways to run that code. + +## Choose how to run code + +| Way | Code runs in | Use when | +| ------------------------------------------------------------ | -------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| [Computer](/agents/runtime/code-mode/computer/) (recommended) | A Dynamic Worker, against workspace files | You want an `exec` tool for the model, with your tools supplied as modules. | +| [The executor](/agents/runtime/code-mode/executor/) | A Dynamic Worker, against your own storage | You have your own file storage or tool layer, or you are building your own code tool. | +| [A container](/agents/tools/sandbox/) | A Linux virtual machine | The code needs a shell, native binaries, package installs, or a build toolchain. | +| [Skill scripts](/agents/runtime/code-mode/skill-scripts/) | A Dynamic Worker or the workspace | You run scripts bundled with [Agent Skills](/agents/skills/). | + +Start with Computer. Drop down to the executor when you need to own the parts that Computer provides. + +## How the packages fit together + +Two packages do the work: + +- **`@cloudflare/codemode`** holds the executor. `WorkerJavaScriptBackend` runs one ECMAScript module in a fresh [Dynamic Worker](/dynamic-workers/). The code reads files through `node:fs` and imports host modules, whose functions run in your Durable Object. +- **`@cloudflare/computer`** builds on that executor. It adds a persistent `Workspace` file system, the `exec` tool and file tools for the model, and prebuilt modules such as `codemode:tools`, `codemode:git`, and `codemode:container`. + +```mermaid +flowchart TB + Model["Model writes code"] -->|"exec tool"| Computer + subgraph Computer["@cloudflare/computer"] + WS["Workspace files"] + Mods["codemode:* modules"] + end + Computer --> Exec + subgraph Exec["@cloudflare/codemode"] + Backend["WorkerJavaScriptBackend"] + end + Backend --> DW["Dynamic Worker
one isolate per run"] +``` + +You supply tools to the code as modules. Each module is imported under a `codemode:` name, and each of its functions runs in your agent, not in the sandbox: + +```js +import { readFile } from "node:fs/promises"; +import { lookup } from "codemode:orders"; + +export default async function () { + const ids = JSON.parse(await readFile("/workspace/orders.json", "utf8")); + return Promise.all(ids.map((id) => lookup(id))); +} +``` + +## Run code in a container + +Some code needs a full Linux environment. Use a container when the model must run shell commands, install packages, or build and test a project. + +- [Sandbox](/agents/tools/sandbox/) attaches a container to an agent and gives the model a tool that runs commands in it. +- Computer can also run a container backend next to its JavaScript backend. With the `codemode:container` module, JavaScript code can call the container, so the model only needs one `exec` tool. Refer to [Call a container from code](/agents/runtime/code-mode/computer/#call-a-container-from-code). + +A container starts slower than a Dynamic Worker and costs more to run. If the code only calls functions that you provide, use Computer or the executor instead. + +## The legacy Code Mode API + +`createCodeTool()`, `DynamicWorkerExecutor`, connectors, the durable approval runtime, and the MCP server helpers in `@cloudflare/codemode` are feature-frozen. They keep working, but they get no new features or fixes. Refer to [Legacy Code Mode API](/agents/runtime/code-mode/legacy/) for what replaces each one. + +## Pages in this section + + + + + + + diff --git a/src/content/docs/agents/runtime/code-mode/legacy.mdx b/src/content/docs/agents/runtime/code-mode/legacy.mdx new file mode 100644 index 00000000000..a9324060e25 --- /dev/null +++ b/src/content/docs/agents/runtime/code-mode/legacy.mdx @@ -0,0 +1,108 @@ +--- +title: Legacy Code Mode API +description: createCodeTool, DynamicWorkerExecutor, connectors, the durable runtime, and the MCP server helpers in @cloudflare/codemode are feature-frozen. Learn what replaces each one. +pcx_content_type: reference +sidebar: + order: 9 +products: + - agents +--- + +import { TypeScriptExample } from "~/components"; + +`@cloudflare/codemode` used to export a full Code Mode toolkit: `createCodeTool()`, `DynamicWorkerExecutor`, connectors, a durable runtime with approvals and snippets, MCP server helpers, a browser executor, and a Vite plugin. That API is now feature-frozen: + +- It keeps working from the same import paths, and upgrading the package does not change it. +- It gets no new features and no fixes. +- Every export is marked `@deprecated`, so your editor flags each use. + +The package's main export is now [`WorkerJavaScriptBackend`](/agents/runtime/code-mode/executor/), the executor that [Computer](/agents/runtime/code-mode/computer/) runs code on. For new agents, use Computer. + +The legacy code lives in [`packages/codemode/src/legacy`](https://github.com/cloudflare/agents/tree/main/packages/codemode/src/legacy). Nothing in that folder imports from outside it. If you rely on a pattern in it, copy the folder into your project and own it from there. + +## What replaces what + +| Legacy API | Replacement | +| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `createCodeTool()` from `/ai` or `/tanstack-ai` | The `exec` tool from `createAITools()`, `createTanStackTools()`, or `createPiTools()` in Computer | +| `DynamicWorkerExecutor`, `Executor` | [`WorkerJavaScriptBackend`](/agents/runtime/code-mode/executor/), on its own or through a Computer workspace | +| Tools as `codemode.*` globals, `ToolProvider` | Tools imported from [`codemode:tools`](/agents/runtime/code-mode/computer/#supply-the-agents-tools) | +| `CodemodeConnector`, `OpenApiConnector` | A [host module](/agents/runtime/code-mode/computer/#supply-an-api) under a `codemode:` name | +| `McpConnector`, `ToolSetConnector` | [`codemode:tools` with MCP tools](/agents/runtime/code-mode/computer/#supply-mcp-tools) | +| `workspace.*` tools in the sandbox | `node:fs` in the code | +| `createCodemodeRuntime()` approvals and replay | [`ctx.interrupt()`](/agents/runtime/code-mode/computer/#ask-for-approval) in a host module | + +These have no replacement: + +- `codeMcpServer()` and `openApiMcpServer()` from `/mcp`. For a server-side Code Mode MCP server, start from the [Cloudflare MCP server source](https://github.com/cloudflare/mcp). Refer to [Code Mode on the server](/agents/concepts/code-mode/#code-mode-on-the-server). +- `IframeSandboxExecutor` and `createBrowserCodeTool()` from `/browser`. +- Rollback, through `revert` and `runtime.rollback()`. +- Snippets, through `saveSnippet()` and `codemode.run()`. Save the code as a workspace file and import it instead. +- The Vite plugin from `/vite`. + +## Move a code tool to Computer + +Before, with the legacy API: + + + +```ts +import { DynamicWorkerExecutor } from "@cloudflare/codemode"; +import { createCodeTool } from "@cloudflare/codemode/ai"; +import { streamText } from "ai"; + +const codemode = createCodeTool({ + tools: myTools, + executor: new DynamicWorkerExecutor({ loader: env.LOADER }), +}); + +streamText({ model, messages, tools: { codemode } }); +``` + + + +After, with Computer: + + + +```ts +import { Workspace } from "@cloudflare/computer"; +import { WorkerJavaScriptBackend } from "@cloudflare/computer/backends/worker-javascript"; +import { createToolBindings } from "@cloudflare/computer/modules/tools"; +import { createAITools, forAISDKTools } from "@cloudflare/computer/tools/ai-sdk"; +import { streamText } from "ai"; + +const workspace = new Workspace({ + storage: this.ctx.storage, + backends: [ + new WorkerJavaScriptBackend({ + loader: this.env.LOADER, + modules: { + "codemode:tools": createToolBindings(forAISDKTools(() => myTools)), + }, + }), + ], +}); + +streamText({ model, messages, tools: createAITools({ workspace }) }); +``` + + + +The model's code changes too. Legacy code was an async arrow function that called `codemode.toolName()`. Computer code is a module with a default export that imports the tools: + +```js +import { getWeather } from "codemode:tools"; + +export default async function () { + return (await getWeather({ city: "Lisbon" })).text; +} +``` + +## Legacy documentation + +The guides for the legacy API are in the Agents SDK repository: + +- [The legacy API](https://github.com/cloudflare/agents/blob/main/docs/codemode/legacy.md): what is frozen, and how to copy it. +- [How the legacy API works](https://github.com/cloudflare/agents/blob/main/docs/codemode/legacy-overview.md): executors, connectors, the durable runtime, approvals, and replay. +- [Legacy API reference](https://github.com/cloudflare/agents/blob/main/docs/codemode/legacy-reference.md): every legacy export and option. diff --git a/src/content/docs/agents/skills/execution.mdx b/src/content/docs/agents/runtime/code-mode/skill-scripts.mdx similarity index 95% rename from src/content/docs/agents/skills/execution.mdx rename to src/content/docs/agents/runtime/code-mode/skill-scripts.mdx index c2e86008161..dc50be111c5 100644 --- a/src/content/docs/agents/skills/execution.mdx +++ b/src/content/docs/agents/runtime/code-mode/skill-scripts.mdx @@ -1,5 +1,5 @@ --- -title: Execution +title: Run skill scripts pcx_content_type: reference description: Run Agent Skills scripts with a Computer Workspace backend, or with the experimental skill script runner. sidebar: @@ -10,7 +10,7 @@ products: import { TypeScriptExample, WranglerConfig } from "~/components"; -[Agent Skills](/agents/skills/) can bundle scripts under `scripts/`. An agent runs them in one of two ways: +[Agent Skills](/agents/skills/) can bundle scripts under `scripts/`. Running one is a form of [Code Mode](/agents/concepts/code-mode/), because a sandbox runs code that the model chose. An agent runs skill scripts in one of two ways: | Where the skill lives | How scripts run | | ------------------------------------------------------ | ------------------------------------------------------------- | @@ -141,4 +141,5 @@ Scripts get no network access and no tools unless you grant them. Relative paths ## Related - [Think tools](/agents/harnesses/think/tools/): how skill tools merge with workspace, custom, MCP, and client tools +- [Run code with Computer](/agents/runtime/code-mode/computer/): give the model an `exec` tool, and your tools as modules - [Sandbox](/agents/tools/sandbox/): run code in a container diff --git a/src/content/docs/agents/runtime/index.mdx b/src/content/docs/agents/runtime/index.mdx index c7b36139f0f..44f356b1c56 100644 --- a/src/content/docs/agents/runtime/index.mdx +++ b/src/content/docs/agents/runtime/index.mdx @@ -18,6 +18,7 @@ Every agent built with the Agents SDK runs on a [Durable Object](/durable-object - **[Routing](/agents/runtime/routing/)** (`agents/routing`) sends requests from your Worker to the right named object. - **[Capabilities](/agents/runtime/capabilities/)** are features you attach with `lifecycle.use()`, such as WebSocket clients, shared state, a scheduler, and MCP connections. - **[Observability](/agents/runtime/observability/)** traces model calls and publishes events from the Lifecycle and its capabilities. +- **[Code Mode](/agents/runtime/code-mode/)** runs code that the model writes, in a sandbox, with your tools supplied as modules. ```mermaid flowchart LR @@ -125,3 +126,10 @@ To write your own capability, refer to [Build a capability](/agents/runtime/capa - [Tracing](/agents/runtime/observability/tracing/) records model calls, tool runs, and approvals as Workers traces. - [Diagnostics channels](/agents/runtime/observability/diagnostics-channels/) publish structured events from the Lifecycle, its capabilities, and the `Agent` class. + +## Code Mode + +- [Code Mode](/agents/runtime/code-mode/) compares the ways to run model-written code. +- [Run code with Computer](/agents/runtime/code-mode/computer/) gives the model an `exec` tool, and supplies your tools as modules. +- [Use the executor](/agents/runtime/code-mode/executor/) runs code with `WorkerJavaScriptBackend` from `@cloudflare/codemode`. +- [Run skill scripts](/agents/runtime/code-mode/skill-scripts/) runs the scripts bundled with Agent Skills. diff --git a/src/content/docs/agents/skills/index.mdx b/src/content/docs/agents/skills/index.mdx index 5914137777e..0276b073a5b 100644 --- a/src/content/docs/agents/skills/index.mdx +++ b/src/content/docs/agents/skills/index.mdx @@ -88,7 +88,7 @@ When at least one skill is available, the agent gets these tools: | --------------------- | ----------------------------------------------------------------------------- | | `activate_skill` | Load a skill's instructions and its list of resources | | `read_skill_resource` | Read a resource by `{ name, path }` or a qualified path such as `name/path` | -| `run_skill_script` | Run a bundled script, when a [script runner](/agents/skills/execution/) is set | +| `run_skill_script` | Run a bundled script, when a [script runner](/agents/runtime/code-mode/skill-scripts/) is set | The catalog is added to the system prompt as its own context block, below your `getSystemPrompt()` instructions. When a skill changes mid-conversation, Think does not rebuild the system prompt, so the model provider's prompt cache stays warm. The new catalog reaches the model as a reminder on the next turn and moves into the system prompt at the next compaction or history clear. diff --git a/src/content/docs/agents/skills/sources/computer.mdx b/src/content/docs/agents/skills/sources/computer.mdx index 56ade0807b8..c2f296c7c96 100644 --- a/src/content/docs/agents/skills/sources/computer.mdx +++ b/src/content/docs/agents/skills/sources/computer.mdx @@ -139,7 +139,7 @@ With the JavaScript backend, a skill can tell the model to run a script by re-ex export { default } from "./scripts/format-release-notes.js"; ``` -Refer to [Execution](/agents/skills/execution/) for the options. +Refer to [Run skill scripts](/agents/runtime/code-mode/skill-scripts/) for the options. ## Example diff --git a/src/content/docs/agents/skills/sources/r2.mdx b/src/content/docs/agents/skills/sources/r2.mdx index 03930668d0c..98fc51e5a93 100644 --- a/src/content/docs/agents/skills/sources/r2.mdx +++ b/src/content/docs/agents/skills/sources/r2.mdx @@ -72,7 +72,7 @@ Each `SKILL.md` is checked against the [specification](/agents/skills/#validatio ## Run skill scripts -R2 stores files but cannot run them. To run scripts from R2 skills, set the experimental [script runner](/agents/skills/execution/#script-runner), which adds the `run_skill_script` tool. To store and run skills as files, use a [Computer Workspace](/agents/skills/sources/computer/) instead. +R2 stores files but cannot run them. To run scripts from R2 skills, set the experimental [script runner](/agents/runtime/code-mode/skill-scripts/#script-runner), which adds the `run_skill_script` tool. To store and run skills as files, use a [Computer Workspace](/agents/skills/sources/computer/) instead. ## Example diff --git a/src/content/docs/agents/tools/browser.mdx b/src/content/docs/agents/tools/browser.mdx index 4945eb18c42..42c3ed523f3 100644 --- a/src/content/docs/agents/tools/browser.mdx +++ b/src/content/docs/agents/tools/browser.mdx @@ -487,7 +487,7 @@ You might still use the legacy tools for the following: - **Your own browser.** Connect to a CDP endpoint you manage with `cdpUrl`. `Browser` supports only Browser Run's Chromium engine. - **Kitesurf.** A [Kitesurf](/browser-run/kitesurf/) browser lives only as long as its CDP WebSocket, so it cannot be a persistent browser. -- **Approval or human handoff within a run.** The [durable Code Mode runtime](/agents/tools/codemode/how-it-works/) can pause a run for approval and resume it with the browser session intact. `browserTool` does not pause runs. +- **Approval or human handoff within a run.** The [legacy Code Mode runtime](/agents/runtime/code-mode/legacy/) can pause a run for approval and resume it with the browser session intact. `browserTool` does not pause runs. You do not need the legacy tools to get a fresh browser for each run. With `browserTool`, call [`browser.close()`](#close-the-browser) after a turn, or set a short `keepAliveMs`, so the next run starts a new browser. To read a page without a browser, use the [`web_fetch` tool](/agents/tools/web-fetch/). @@ -537,7 +537,7 @@ Browser tools must be created from inside a Durable Object, such as an Agent. Th | `browser_links` | List links on a page. | | `browser_scrape` | Scrape specific elements by CSS selector. | -The TanStack AI `createBrowserTools` returns only `browser_execute`. To discover protocol surface, the model calls `cdp.spec()` or the runtime's built-in [`codemode.search()` and `codemode.describe()`](/agents/tools/codemode/api-reference/#sandbox-codemode-api). +The TanStack AI `createBrowserTools` returns only `browser_execute`. To discover protocol surface, the model calls `cdp.spec()` or the runtime's built-in `codemode.search()` and `codemode.describe()`. Refer to the [legacy Code Mode API](/agents/runtime/code-mode/legacy/). ### Configuration diff --git a/src/content/docs/agents/tools/codemode/ai-sdk.mdx b/src/content/docs/agents/tools/codemode/ai-sdk.mdx deleted file mode 100644 index 62c521e80bd..00000000000 --- a/src/content/docs/agents/tools/codemode/ai-sdk.mdx +++ /dev/null @@ -1,255 +0,0 @@ ---- -title: AI SDK integration -description: Use AI SDK tools with Code Mode through createCodeTool(), namespaced providers, or ToolSetConnector for durable execution. -pcx_content_type: integration-guide -sidebar: - order: 2 -products: - - agents ---- - -import { - PackageManagers, - Steps, - TypeScriptExample, - WranglerConfig, -} from "~/components"; - -The `@cloudflare/codemode/ai` entry point converts AI SDK tools into one Code Mode tool. The model writes JavaScript that calls your tools, and an executor runs that code in an isolated sandbox. - -Choose between two integration patterns: - -| Pattern | Use case | Approval behavior | -| ------------------------------------------ | ----------------------------------------------------------- | ------------------------------------------------ | -| `createCodeTool()` | Simple, stateless execution with one or more tool providers | Excludes tools that use `needsApproval` | -| `ToolSetConnector` or `toolSetConnector()` | Durable execution through a Code Mode runtime | Maps `needsApproval` to durable runtime approval | - -## Create a stateless Code Mode tool - -`createCodeTool()` accepts an AI SDK `ToolSet` or an array of tool providers. It also requires an executor. It returns a standard AI SDK tool for use with `streamText()` or `generateText()`. - - - -1. Install Code Mode, the AI SDK, and Zod: - - - -2. Add a Worker Loader binding for `DynamicWorkerExecutor`: - - - - ```toml - compatibility_date = "$today" - compatibility_flags = ["nodejs_compat"] - - [[worker_loaders]] - binding = "LOADER" - ``` - - - -3. Define executable AI SDK tools. Code Mode uses their schemas to generate types and validate arguments before calling `execute`. - - - - ```ts - import { tool } from "ai"; - import { z } from "zod"; - - export const weatherTools = { - getWeather: tool({ - description: "Get the weather for a city", - inputSchema: z.object({ - city: z.string().describe("City name") - }), - outputSchema: z.object({ - city: z.string(), - conditions: z.string() - }), - execute: async ({ city }) => ({ - city, - conditions: "sunny" - }) - }) - }; - ``` - - - - Each sandbox-callable tool needs an `execute` function. Client-side or provider-executed tools cannot run through this server-side executor. - -4. Create the Code Mode tool and pass it to an AI SDK model call: - - - - ```ts - import { DynamicWorkerExecutor } from "@cloudflare/codemode"; - import { createCodeTool } from "@cloudflare/codemode/ai"; - import { generateText, stepCountIs } from "ai"; - import { model } from "./model"; - import { weatherTools } from "./tools"; - - export default { - async fetch(request, env): Promise { - const executor = new DynamicWorkerExecutor({ loader: env.LOADER }); - const codemode = createCodeTool({ tools: weatherTools, executor }); - - const response = await generateText({ - model, - prompt: await request.text(), - tools: { codemode }, - stopWhen: stepCountIs(5), - }); - - return new Response(response.text); - }, - } satisfies ExportedHandler; - ``` - - - - - -The example uses `generateText()` for a completed response. You can pass the same `codemode` tool to `streamText()` for streaming. The generated tool description includes TypeScript definitions for `getWeather`. The model still writes JavaScript, such as: - -```js -async () => { - const weather = await codemode.getWeather({ city: "Lisbon" }); - return weather.conditions; -}; -``` - -The default namespace is `codemode`. `createCodeTool()` also accepts a custom `description`. Include `{{types}}` in that description where Code Mode should insert the generated definitions. - -## Organize tools with providers - -A tool provider groups tools under one sandbox namespace. Pass the tool set directly when every tool belongs under `codemode.*`. - -Use `aiTools()` when combining AI SDK tools with providers from other packages. The following optional workspace example also requires [`@cloudflare/computer`](https://github.com/cloudflare/computer): - - - - - -```ts -import { AIChatAgent } from "@cloudflare/ai-chat"; -import { DynamicWorkerExecutor } from "@cloudflare/codemode"; -import { aiTools, createCodeTool } from "@cloudflare/codemode/ai"; -import { Workspace } from "@cloudflare/computer"; -import { createAITools } from "@cloudflare/computer/tools/ai-sdk"; -import { weatherTools } from "./tools"; - -export class Chat extends AIChatAgent { - workspace = new Workspace({ storage: this.ctx.storage }); - - codemodeTool() { - return createCodeTool({ - tools: [ - aiTools(weatherTools), - { - name: "workspace", - tools: createAITools({ workspace: this.workspace }), - }, - ], - executor: new DynamicWorkerExecutor({ loader: this.env.LOADER }), - }); - } -} -``` - - - -This example exposes AI SDK tools as `codemode.*` and workspace tools as `workspace.*`, for example `workspace.read({ path })`. - -To assign custom namespaces, pass provider objects instead: - - - -```ts -const executor = new DynamicWorkerExecutor({ loader: env.LOADER }); - -const codemode = createCodeTool({ - tools: [ - { name: "weather", tools: weatherTools }, - { name: "notifications", tools: notificationTools }, - ], - executor, -}); -``` - - - -The generated code can then call `weather.getWeather()` and `notifications.send()`. Provider names must be unique, valid JavaScript identifiers. - -## Use AI SDK tools with the durable runtime - -Use `ToolSetConnector` or its `toolSetConnector()` convenience function when runs need durable state. The connector adapts an AI SDK `ToolSet` for `createCodemodeRuntime()`. The helper returns `new ToolSetConnector(ctx, options)`. - -Create the connector from inside an Agent or another Durable Object: - - - -```ts -import { AIChatAgent } from "@cloudflare/ai-chat"; -import { - createCodemodeRuntime, - DynamicWorkerExecutor, -} from "@cloudflare/codemode"; -import { toolSetConnector } from "@cloudflare/codemode/ai"; -import { convertToModelMessages, streamText } from "ai"; -import { model } from "./model"; -import { operationTools } from "./tools"; - -// Export this manually when the @cloudflare/codemode/vite plugin is not configured. -export { CodemodeRuntime } from "@cloudflare/codemode"; - -export class OperationsAgent extends AIChatAgent { - async onChatMessage() { - const operations = toolSetConnector(this.ctx, { - name: "operations", - instructions: "Use these tools to manage customer requests.", - tools: operationTools, - }); - - const runtime = createCodemodeRuntime({ - ctx: this.ctx, - executor: new DynamicWorkerExecutor({ loader: this.env.LOADER }), - connectors: [operations], - }); - - const result = streamText({ - model, - messages: await convertToModelMessages(this.messages), - tools: { codemode: runtime.tool() }, - }); - - return result.toUIMessageStreamResponse(); - } -} -``` - - - -The example exports `CodemodeRuntime` manually. If you configure the Code Mode Vite plugin as described in [Create a durable Code Mode runtime](/agents/tools/codemode/durable-runtime/), remove that manual export because the plugin adds it. - -The connector defaults to the `tools` namespace when `name` is omitted. It excludes tools without an `execute` function from both generated types and sandbox bindings. - -The durable runtime adds an execution log, pause and resume behavior, and on-demand connector discovery. The model can find methods with `codemode.search()` and inspect their types with `codemode.describe()`. - -## Approval behavior - -The two integration patterns handle AI SDK approvals differently. - -### `createCodeTool()` approvals - -`createCodeTool()` filters out tools where `needsApproval` is `true` or a function. Filtered tools do not appear in generated types and cannot run from sandbox code. A tool with `needsApproval: false` remains available. - -This stateless path does not pause execution for AI SDK approval. Use a standard AI SDK tool outside Code Mode if that tool needs the AI SDK approval flow. - -### `ToolSetConnector` approvals - -`ToolSetConnector` maps AI SDK `needsApproval` to the durable runtime's `requiresApproval` annotation. Calling that tool pauses the run. Your application can inspect pending actions and resume the same execution with `runtime.approve({ executionId })`. - -A function-valued `needsApproval` cannot be evaluated before the sandbox supplies arguments. The connector therefore treats the tool as always requiring approval. `needsApproval: false` executes without pausing. - -This approval uses the Code Mode runtime's durable pause, approval, and replay flow. It does not use the AI SDK per-call approval flow. diff --git a/src/content/docs/agents/tools/codemode/api-reference.mdx b/src/content/docs/agents/tools/codemode/api-reference.mdx deleted file mode 100644 index 015362e1142..00000000000 --- a/src/content/docs/agents/tools/codemode/api-reference.mdx +++ /dev/null @@ -1,862 +0,0 @@ ---- -title: Code Mode API reference -description: Reference the public classes, functions, options, runtime methods, connector hooks, and result types exported by Code Mode. -pcx_content_type: reference -sidebar: - order: 8 -products: - - agents ---- - -Code Mode publishes six package entry points. Import framework-specific APIs from their matching entry point: - -| Entry point | Purpose | -| ---------------------------------- | -------------------------------------------------------------------------- | -| `@cloudflare/codemode` | Runtime, connectors, Workers executor, and framework-independent utilities | -| `@cloudflare/codemode/ai` | AI SDK tools and connector adapter | -| `@cloudflare/codemode/mcp` | Model Context Protocol (MCP) server wrappers | -| `@cloudflare/codemode/tanstack-ai` | TanStack AI tools and adapter | -| `@cloudflare/codemode/browser` | Browser tool descriptor and iframe executor | -| `@cloudflare/codemode/vite` | Vite plugin for connector discovery and Worker exports | - -## `@cloudflare/codemode` - -The main entry point does not require the optional AI SDK, TanStack AI, or Zod peer dependencies to be installed. - -### Runtime construction - -#### `createCodemodeRuntime()` - -```ts -function createCodemodeRuntime( - options: CreateCodemodeRuntimeOptions, -): CodemodeRuntimeHandle; -``` - -Creates the host-side control plane for a named Code Mode runtime. - -`CreateCodemodeRuntimeOptions` has these fields: - -| Field | Type | Required | Description | -| ----------------- | --------------------- | -------- | ----------------------------------------------------------------------------------------------------------- | -| `ctx` | `DurableObjectState` | Yes | Durable Object state that hosts the runtime facet. | -| `connectors` | `CodemodeConnector[]` | Yes | Connectors exposed as sandbox globals. Connector names must be unique, and `codemode` is reserved. | -| `executor` | `Executor` | Yes | Sandbox that runs generated code. | -| `name` | `string` | No | Durable runtime identity. Defaults to `"default"`. Valid characters are letters, digits, `_`, `-`, and `.`. | -| `maxExecutions` | `number` | No | Terminal records kept when a new run begins. Defaults to `50`. Running and paused executions are not pruned. | -| `transformResult` | `TransformResult` | No | Reshapes a completed result returned to the model. The audit trail retains the unmodified result. | - -```ts -interface CodemodeRuntimeHandle { - tool( - options?: CodemodeRuntimeToolOptions, - ): Tool; - execute(input: ProxyToolInput): Promise; - search(query: string): Promise; - describe(target: string): Promise; - approve(options: CodemodeApproveOptions): Promise; - reject(options: CodemodeRejectOptions): Promise; - rollback(options: CodemodeRollbackOptions): Promise; - pending(executionId?: string): Promise; - expirePaused(options?: CodemodeExpireOptions): Promise; - executions(limit?: number): Promise; - deleteExecution(id: string): Promise; - pruneExecutions(keep?: number): Promise; - saveSnippet(name: string, options: SaveSnippetOptions): Promise; - snippets(): Promise; - deleteSnippet(name: string): Promise; -} -``` - -The handle methods have these effects: - -| Method | Effect | -| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `tool(options?)` | Returns the AI SDK tool given to the model. `description` replaces the default description. `connectorHints` adds a one-line hint for each connector when using the default description. | -| `execute({ code })` | Runs code directly without adapting the runtime to an AI SDK tool. The result can complete, pause for approval, or return an error status. | -| `search(query)` | Searches connector methods and saved snippets without running sandbox code. | -| `describe(target)` | Returns on-demand TypeScript documentation for a connector, method, or saved snippet. | -| `approve({ executionId })` | Resumes a paused execution through replay. The result can complete, pause again, or return an error status. It does not revive a non-paused execution. | -| `reject({ seq, executionId })` | Rejects one pending action and terminates the execution. Returns `false` if the action is no longer pending. It does not roll back earlier actions. | -| `rollback({ executionId })` | Calls available `revert` functions in reverse call order. Missing connectors and methods without `revert` remain applied. It attempts later reverts after a failure. | -| `pending(executionId?)` | Lists pending actions. Without an ID, it combines actions from all paused executions. | -| `expirePaused({ maxAgeMs? })` | Terminates stale paused or running executions and returns their IDs. The default age is 24 hours. | -| `executions(limit?)` | Returns audit records, newest first. | -| `deleteExecution(id)` | Deletes one audit record. It also disposes resources for a non-terminal execution. Returns whether the record existed. | -| `pruneExecutions(keep?)` | Deletes older terminal records and returns the count deleted. Defaults to keeping `50`. | -| `saveSnippet(name, options)` | Saves code from `options.executionId` as a reusable snippet. It accepts any execution status, so applications should verify successful completion first. Replaces the same name. | -| `snippets()` | Returns saved snippets, ordered by name. | -| `deleteSnippet(name)` | Deletes a snippet and returns whether it existed. | - -The method option types are: - -```ts -type CodemodeRuntimeToolOptions = { - description?: string; - connectorHints?: Record; -}; - -type CodemodeApproveOptions = { executionId: string }; -type CodemodeRejectOptions = { seq: number; executionId: string }; -type CodemodeRollbackOptions = { executionId: string }; -type CodemodeExpireOptions = { maxAgeMs?: number }; -``` - -#### `CodemodeRuntime` - -```ts -class CodemodeRuntime extends DurableObject { - constructor(ctx: DurableObjectState, env: unknown); -} -``` - -`CodemodeRuntime` is the durable facet behind the runtime handle. The Vite plugin exports this class from the Worker entry module. Use `createCodemodeRuntime()` for application code instead of constructing the facet directly. - -The main entry point also exports these runtime constants: - -| Constant | Value | Purpose | -| ------------------------- | ------------ | -------------------------------------------- | -| `DEFAULT_MAX_EXECUTIONS` | `50` | Default terminal execution retention count | -| `DEFAULT_PAUSED_TTL_MS` | `86400000` | Default stale execution age in milliseconds (24 hours) | -| `MAX_DURABLE_VALUE_BYTES` | `1000000` | Serialized JavaScript string-length limit for one durable value | - -### Runtime tool input and output - -```ts -type ProxyToolInput = { code: string }; - -type ProxyToolOutput = - | { - status: "completed"; - executionId: string; - result: unknown; - logs?: string[]; - } - | { - status: "paused"; - executionId: string; - pending: PendingAction[]; - } - | { - status: "error"; - executionId: string; - error: string; - logs?: string[]; - }; - -type TransformResult = (result: unknown) => unknown | Promise; -``` - -Sandbox and replay errors use the `error` output variant. They do not throw through the model tool call. - -### Execution records - -```ts -type ExecutionStatus = - | "running" - | "paused" - | "completed" - | "error" - | "rejected" - | "rolled_back"; - -type ExecutionState = { - id: string; - code: string; - status: ExecutionStatus; - log: ToolLogEntry[]; - result?: unknown; - error?: string; - logs?: string[]; - connectors?: string[]; - createdAt: number; - updatedAt: number; -}; - -type ToolLogEntry = { - seq: number; - connector: string; - method: string; - args: unknown; - result?: unknown; - requiresApproval: boolean; - ephemeral?: boolean; - state: "executing" | "applied" | "pending" | "reverted" | "error"; -}; - -type PendingAction = { - executionId: string; - seq: number; - connector: string; - method: string; - args: unknown; -}; -``` - -`createdAt` and `updatedAt` contain epoch milliseconds. An ephemeral log entry comes from a connector tool with `replay: "reexecute"`. Its result is not stored and the call runs again during replay. - -The runtime decision type is: - -```ts -type ToolDecision = - | { kind: "replay"; result: unknown } - | { kind: "execute"; seq: number } - | { kind: "pause"; seq: number }; -``` - -### Sandbox `codemode` API - -`runtime.tool()` injects a `codemode` global into generated sandbox code. - -```ts -declare const codemode: { - search(query: string): Promise; - describe(target: string): Promise; - step(name: string, fn: () => T | Promise): Promise; - run(name: string, input?: unknown): Promise; -}; -``` - -The sandbox methods behave as follows: - -| Method | Description | -| ------------------- | ----------------------------------------------------------------------------------------------------------------- | -| `search(query)` | Searches connector methods and saved snippets. Results are ranked and limited to `50`. | -| `describe(target)` | Returns generated TypeScript for a connector, `connector.method`, or snippet name. | -| `step(name, fn)` | Runs a closure once and records its result. Replay returns the recorded result without running the closure again. | -| `run(name, input?)` | Runs a saved snippet. A missing snippet or recorded connector resolves to an object with an `error` property. | - -Use `step()` around nondeterministic or side-effectful sandbox work that does not use a connector. Issue connector calls sequentially when an execution can pause. Concurrent calls can reach the replay cursor in a different order. - -The discovery output types are: - -```ts -type SearchResult = { - path: string; - connector: string; - method: string; - description?: string; - requiresApproval?: boolean; - kind: "method" | "snippet"; - score: number; -}; - -type SearchOutput = { - results: SearchResult[]; - total: number; - truncated: boolean; -}; - -type DescribeOutput = { - path: string; - description?: string; - requiresApproval?: boolean; - types: string; - kind: "connector" | "method" | "snippet"; -}; -``` - -`requiresApproval` is `true` when a connector method pauses before execution. It is omitted for methods that do not require approval and for snippets. - -### Snippet types - -```ts -interface SaveSnippetOptions { - description?: string; - inputSchema?: unknown; - executionId: string; -} - -interface Snippet { - name: string; - description: string; - code: string; - savedAt: number; - inputSchema?: unknown; - connectors?: string[]; -} -``` - -`connectors` records every namespace configured when the source execution started. `savedAt` contains epoch milliseconds. Before calling `saveSnippet()`, verify that the source `ExecutionState.status` is `completed`. - -### Executor API - -#### `Executor` - -```ts -interface Executor { - execute( - code: string, - providersOrFns: - | ResolvedProvider[] - | Record Promise>, - options?: ExecuteOptions, - ): Promise; -} -``` - -Custom executors should report failures in `ExecuteResult.error` instead of throwing. - -```ts -interface ExecuteResult { - result: unknown; - error?: string; - logs?: string[]; -} - -interface ResolvedProvider { - name: string; - fns: Record Promise>; - prelude?: string; -} - -interface ConnectorBinding { - name: string; - binding: { - callTool(method: string, args: unknown): Promise; - }; -} - -interface ExecuteOptions { - connectors?: ConnectorBinding[]; -} -``` - -Passing a function record instead of `ResolvedProvider[]` is deprecated. It creates one provider named `codemode`. - -#### `DynamicWorkerExecutor` - -```ts -class DynamicWorkerExecutor implements Executor { - constructor(options: DynamicWorkerExecutorOptions); - execute( - code: string, - providersOrFns: - | ResolvedProvider[] - | Record Promise>, - options?: ExecuteOptions, - ): Promise; -} -``` - -`DynamicWorkerExecutorOptions` has these fields: - -| Field | Type | Required | Default | Description | -| ---------------- | ------------------------- | -------- | ------- | ------------------------------------------------------------------------------------------ | -| `loader` | `WorkerLoader` | Yes | — | Worker Loader binding used to create isolated Workers. | -| `timeout` | `number` | No | `60000` | Execution timeout in milliseconds. | -| `globalOutbound` | `Fetcher \| null` | No | `null` | Outbound network policy. `null` blocks access. A `Fetcher` receives all outbound requests. | -| `modules` | `Record` | No | `{}` | Module source keyed by import specifier. The reserved `executor.js` key is ignored. | -| `bindings` | `Record` | No | `{}` | Additional environment bindings injected into each sandbox Worker. | - -The executor validates provider and connector namespaces. Names must be valid JavaScript identifiers, unique, and must not shadow executor globals. - -#### `ToolDispatcher` - -```ts -class ToolDispatcher extends RpcTarget { - constructor(fns: Record Promise>); - call(name: string, argsJson?: string): Promise; -} -``` - -`ToolDispatcher` is the Workers RPC bridge used by `DynamicWorkerExecutor`. `call()` accepts serialized positional arguments and returns a serialized result or error envelope. - -#### `runCode()` - -```ts -function runCode(options: { - code: string; - executor: Executor; - providers: ResolvedProvider[]; - connectors?: ConnectorBinding[]; -}): Promise<{ result: unknown; logs?: string[] }>; -``` - -Normalizes and executes code. An `ExecuteResult.error` causes `runCode()` to throw an `Error` that includes captured console output. - -### Tool providers - -```ts -interface ToolProvider { - name?: string; - tools: ToolDescriptors | ToolSet | SimpleToolRecord; - types?: string; -} -``` - -Tool providers have these fields: - -| Field | Description | -| ------- | ---------------------------------------------------------------------------------- | -| `name` | Sandbox namespace. Defaults to `codemode`. | -| `tools` | Tool descriptors, an AI SDK `ToolSet`, or records containing `execute`. | -| `types` | TypeScript declarations shown to the model. Code Mode generates them when omitted. | - -```ts -function resolveProvider(provider: ToolProvider): ResolvedProvider; -``` - -The main-entry implementation does not validate inputs against schemas. It excludes tools whose `needsApproval` is `true` or a function. Use runtime connectors for durable approval flows. - -### Connector base classes - -#### `CodemodeConnector` - -```ts -abstract class CodemodeConnector< - Env = unknown, - Props = unknown, -> extends WorkerEntrypoint { - constructor(ctx: DurableObjectState | ExecutionContext, env: Env); - - abstract name(): string; - protected instructions(): string | undefined; - protected abstract tools(): ConnectorTools | Promise; - protected tool(name: string, tool: ConnectorTool): ConnectorTool; - - describe(): Promise; - executeTool( - method: string, - args: unknown, - ctx?: ToolExecuteContext, - ): Promise; - revertAction( - method: string, - args: unknown, - result: unknown, - ctx?: ToolExecuteContext, - ): Promise; - onPassEnd(executionId: string, status: PassEndStatus): Promise; - disposeExecution( - executionId: string, - status: ExecutionEndStatus, - ): Promise; - getTypeScriptTypes(): Promise; -} -``` - -Connector authors implement or override these hooks: - -| Hook | Required | Description | -| --------------------------------------- | -------- | ----------------------------------------------------------------------------------------------- | -| `name()` | Yes | Returns the unique sandbox namespace. | -| `instructions()` | No | Returns connector guidance included by `describe()`. | -| `tools()` | Yes | Returns the connector tool record. Derived connectors implement this hook. | -| `tool(name, tool)` | No | Decorates a resolved tool. Use it to add approval, replay, or revert behavior to derived tools. | -| `onPassEnd(executionId, status)` | No | Releases per-pass resources. Runs after every pass, including a paused pass. | -| `disposeExecution(executionId, status)` | No | Releases per-execution resources after a terminal transition. It does not run on pause. | - -Lifecycle hooks should be idempotent, should not rely on instance memory, and should not throw. On a terminal pass, `onPassEnd()` runs before `disposeExecution()`. - -The base class derives `describe()`, `executeTool()`, `revertAction()`, and `getTypeScriptTypes()` from the tool record. Connector authors do not need to implement these methods. - -#### Connector tool types - -```ts -type ConnectorTool = { - description?: string; - inputSchema?: JSONSchema7; - outputSchema?: JSONSchema7; - requiresApproval?: boolean; - replay?: "log" | "reexecute"; - execute: ( - args: unknown, - ctx?: ToolExecuteContext, - ) => Promise | unknown; - revert?: ( - args: unknown, - result: unknown, - ctx?: ToolExecuteContext, - ) => Promise | void; -}; - -type ConnectorTools = Record; -type ToolExecuteContext = { executionId: string }; -``` - -`inputSchema` defaults to an open object. `requiresApproval: true` pauses before execution. `replay: "reexecute"` skips durable result storage and re-executes the call on each resume. These two options cannot be combined. - -`revert` provides compensation for `runtime.rollback()`. It can apply to any tool, whether or not the tool requires approval. - -#### `McpConnector` - -```ts -abstract class McpConnector< - Env = unknown, - Props = unknown, -> extends CodemodeConnector { - protected abstract createConnection(): - | McpConnectionLike - | Promise; - protected toolName(tool: McpTool): string; -} -``` - -`McpConnector` converts each MCP tool into a connector method. `toolName()` defaults to `sanitizeToolName(tool.name)`. Override it to resolve naming collisions. - -```ts -interface McpConnectionLike { - name?: string; - client: Pick; - instructions?: string; - tools?: McpTool[]; - fetchTools?: () => Promise; -} -``` - -The connector uses `tools` when that array is non-empty. Otherwise, it calls `fetchTools()` when provided. MCP error results become thrown connector errors. Structured content is returned before text content. - -#### `OpenApiConnector` - -```ts -abstract class OpenApiConnector< - Env = unknown, - Props = unknown, -> extends CodemodeConnector { - protected abstract spec(): - | Record - | Promise>; - protected abstract request(options: OpenApiRequestOptions): Promise; - protected exposeSpec(): boolean; -} -``` - -`OpenApiConnector` creates one method per OpenAPI operation. It uses a sanitized `operationId` when present, then falls back to a name based on the HTTP method and path. Duplicate operations and names reserved for `request` or `spec` are skipped. - -Every OpenAPI connector exposes a low-level `request` method. `exposeSpec()` defaults to `false`. Return `true` to also expose `spec`. - -```ts -type OpenApiRequestOptions = { - path: string; - method?: string; - params?: Record; - body?: unknown; - headers?: Record; -}; -``` - -Derived operation tools substitute path parameters. They pass query values as `params`, header values as `headers`, and JSON request data as `body`. - -#### Connector lifecycle and description types - -```ts -type ExecutionEndStatus = "completed" | "error" | "rejected" | "rolled_back"; - -type PassEndStatus = ExecutionEndStatus | "paused"; - -type ToolAnnotations = { - requiresApproval?: boolean; - replay?: "log" | "reexecute"; -}; - -type ConnectorDescription = { - name: string; - instructions?: string; - descriptors: JsonSchemaToolDescriptors; - annotations?: Record; -}; -``` - -### JSON Schema utilities - -```ts -interface JsonSchemaToolDescriptor { - description?: string; - inputSchema: JSONSchema7; - outputSchema?: JSONSchema7; -} - -type JsonSchemaToolDescriptors = Record; - -function generateTypesFromJsonSchema(tools: JsonSchemaToolDescriptors): string; - -function jsonSchemaToType(schema: JSONSchema7, typeName: string): string; -``` - -`generateTypesFromJsonSchema()` returns declarations for a `codemode` namespace. Tool names are sanitized before declarations are generated. Unsupported schemas degrade to `unknown` instead of causing generation to fail. - -### Code and output utilities - -The main entry point provides these code and result utilities: - -| Function | Signature | Behavior | -| ------------------ | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -| `sanitizeToolName` | `(name: string) => string` | Replaces common separators, removes invalid characters, prefixes digit-leading names, and suffixes JavaScript reserved words. | -| `normalizeCode` | `(code: string) => string` | Converts common model output forms into an async arrow function. It also removes supported Markdown fences. | -| `truncateResponse` | `(text: string, options?: TruncateOptions) => string` | Truncates text to a character budget and appends a size marker. | -| `truncateResult` | `(value: unknown, options?: TruncateOptions) => unknown` | Preserves small structured values. Oversized serializable values become truncated JSON text. | - -```ts -type TruncateOptions = { - maxChars?: number; - maxTokens?: number; -}; -``` - -The default budget is `6000` estimated tokens at four characters per token. `maxChars` overrides the derived character budget. - -## `@cloudflare/codemode/ai` - -This entry point requires the `ai` and `zod` peer dependencies. - -### `createCodeTool()` - -```ts -function createCodeTool( - options: CreateCodeToolOptions, -): Tool; - -interface CreateCodeToolOptions { - tools: ToolProviderTools | ToolProvider[]; - executor: Executor; - description?: string; -} - -type CodeInput = { code: string }; -type CodeOutput = { result: unknown; logs?: string[] }; -``` - -`description` can contain `{{types}}`. Code Mode replaces that token with generated declarations. A raw tool record becomes one provider named `codemode`. An array accepts multiple provider namespaces. - -Tools whose `needsApproval` is `true` or a function are excluded. This API does not pause. Use `createCodemodeRuntime()` and connectors for durable approval handling. - -### AI SDK provider utilities - -The AI SDK entry point provides these tool-provider utilities: - -| Export | Signature | Description | -| ----------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -| `aiTools` | `(tools: ToolDescriptors \| ToolSet) => ToolProvider` | Wraps AI SDK tools in the default provider. | -| `generateTypes` | `(tools: ToolDescriptors \| ToolSet, namespace?: string) => string` | Generates declarations from AI SDK or Zod schemas. The namespace defaults to `codemode`. | -| `resolveProvider` | `(provider: ToolProvider) => ResolvedProvider` | Filters approval-gated tools, validates input with AI SDK `asSchema()` when available, and extracts executable functions. | - -```ts -interface ToolDescriptor { - description?: string; - inputSchema: ZodType; - outputSchema?: ZodType; - execute?: (args: unknown) => Promise; -} - -type ToolDescriptors = Record; -``` - -### `ToolSetConnector` - -```ts -class ToolSetConnector extends CodemodeConnector { - constructor( - ctx: DurableObjectState | ExecutionContext, - options: ToolSetConnectorOptions, - ); -} - -function toolSetConnector( - ctx: DurableObjectState | ExecutionContext, - options: ToolSetConnectorOptions, -): ToolSetConnector; - -interface ToolSetConnectorOptions { - name?: string; - instructions?: string; - tools: ToolSet; -} -``` - -The namespace defaults to `tools`. The connector excludes tools without an `execute` function. `needsApproval: true` and function-valued `needsApproval` map to durable connector approval. `needsApproval: false` executes without approval. AI SDK schemas validate input before execution. - -## `@cloudflare/codemode/mcp` - -This entry point requires the MCP SDK and Zod peer dependencies. - -### `codeMcpServer()` - -```ts -interface CodeMcpServerOptions { - server: McpServer; - executor: Executor; - description?: string; -} - -function codeMcpServer(options: CodeMcpServerOptions): Promise; -``` - -Wraps an existing MCP server with one `code` tool. The wrapper connects to the source server through an in-memory transport, discovers its tools, and exposes those tools as methods on `codemode` inside the executor. - -A custom description can contain `{{types}}`, which the wrapper replaces with generated TypeScript declarations. It can also contain `{{example}}`, which the wrapper replaces with an example call based on the first upstream MCP tool. Returned MCP values are unwrapped in this order: compatibility `toolResult`, MCP errors, `structuredContent`, all-text content, then the original mixed-content result. - -### `openApiMcpServer()` - -```ts -interface OpenApiMcpServerOptions { - spec: Record; - executor: Executor; - request: (options: RequestOptions) => Promise; - name?: string; - version?: string; - description?: string; -} - -interface RequestOptions { - method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; - path: string; - query?: Record; - body?: unknown; - contentType?: string; - rawBody?: boolean; -} - -function openApiMcpServer(options: OpenApiMcpServerOptions): McpServer; -``` - -Creates an MCP server with two tools: - -| MCP tool | Sandbox API | Purpose | -| --------- | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | -| `search` | `codemode.spec()` | Runs code against the OpenAPI document. Local `$ref` values are resolved before the code receives the document. | -| `execute` | `codemode.spec()` and `codemode.request(options)` | Runs code that can inspect the document and call the host-provided request function. | - -`name` defaults to `openapi`. `version` defaults to `1.0.0`. The host request function keeps credentials outside the sandbox. Text responses are limited to approximately 6,000 tokens and include a truncation marker when clipped. - -The `search` and `execute` tool descriptions use fixed example snippets. Unlike `codeMcpServer()`, this function does not support `{{types}}` or `{{example}}` placeholders. An optional `description` is appended to the `execute` tool description. - -## `@cloudflare/codemode/tanstack-ai` - -This entry point requires the `@tanstack/ai` and `zod` peer dependencies. - -### `createCodeTool()` - -```ts -function createCodeTool(options: CreateCodeToolOptions): ServerTool; -``` - -The options, `CodeInput`, and `CodeOutput` match the `/ai` entry point. The returned `ServerTool` can be passed to TanStack AI `chat()`. - -### TanStack AI provider utilities - -The TanStack AI entry point provides these tool-provider utilities: - -| Export | Signature | Description | -| -------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `tanstackTools` | `(tools: TanStackTool[], name?: string) => ToolProvider` | Wraps TanStack AI tools in a provider. Only tools with an `execute` function are callable. The namespace defaults to `codemode`. | -| `generateTypes` | `(tools: TanStackTool[], namespace?: string) => string` | Converts supported TanStack AI schemas to JSON Schema, then generates declarations. | -| `resolveProvider` | `(provider: ToolProvider) => ResolvedProvider` | Resolves a framework-independent provider without schema validation. | -| `normalizeProviders` | `(tools: ToolProviderTools \| ToolProvider[]) => ToolProvider[]` | Converts raw tools into a one-element provider array. | - -This entry point also exports `DEFAULT_DESCRIPTION`. `tanstackTools()` excludes tools with `needsApproval: true` or a function-valued `needsApproval`. Tools with `needsApproval: false` remain callable. - -## `@cloudflare/codemode/browser` - -The browser entry point uses browser APIs and plain JSON Schema. It does not require the AI SDK or Zod. - -### `createBrowserCodeTool()` - -```ts -function createBrowserCodeTool( - options: CreateBrowserCodeToolOptions, -): BrowserCodeToolDescriptor; - -interface CreateBrowserCodeToolOptions { - tools: - | JsonSchemaExecutableToolDescriptor[] - | JsonSchemaExecutableToolDescriptors; - executor?: Executor; - description?: string; -} -``` - -Array-form tools must include `name`. Object-form tools use each record key as the name. The executor defaults to a new `IframeSandboxExecutor`. - -The `tools` option also accepts descriptors with `needsApproval?: boolean | ((...args: unknown[]) => unknown)`. Tools with `needsApproval: true` or a function-valued `needsApproval` are excluded. Tools with `needsApproval: false` remain callable. JSON Schema contributes model-facing declarations but does not perform runtime validation. - -```ts -interface JsonSchemaExecutableToolDescriptor extends JsonSchemaToolDescriptor { - name?: string; - execute: (args: Record) => Promise; -} - -type JsonSchemaExecutableToolDescriptors = Record< - string, - JsonSchemaExecutableToolDescriptor ->; -``` - -The returned descriptor has this shape: - -```ts -interface BrowserCodeToolDescriptor { - name: string; - description: string; - inputSchema: { - type: "object"; - properties: { - code: { type: "string"; description: string }; - }; - required: ["code"]; - }; - outputSchema: { - type: "object"; - properties: { - result: { description: string }; - logs: { - type: "array"; - items: { type: "string" }; - description: string; - }; - }; - required: ["result"]; - }; - execute(args: CodeInput): Promise; -} -``` - -### `IframeSandboxExecutor` - -```ts -class IframeSandboxExecutor implements Executor { - constructor(options?: IframeSandboxExecutorOptions); - execute( - code: string, - providersOrFns: - | ResolvedProvider[] - | Record Promise>, - ): Promise; -} - -interface IframeSandboxExecutorOptions { - timeout?: number; - csp?: string; -} -``` - -The iframe executor accepts these options: - -| Field | Default | Description | -| --------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | -| `timeout` | `30000` | Maximum execution time in milliseconds. It cannot preempt a synchronous loop that blocks the browser event loop. | -| `csp` | `default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval';` | Content Security Policy applied to the sandbox iframe document. | - -Each execution creates a hidden iframe with `sandbox="allow-scripts"`. Tool calls cross the iframe boundary through nonce-scoped `postMessage` messages. The iframe is removed after success, error, or timeout. - -This entry point also exports the framework-independent `Executor`, `ExecuteResult`, and `ResolvedProvider` types. It re-exports `JsonSchemaToolDescriptor` and `JsonSchemaToolDescriptors`. - -## `@cloudflare/codemode/vite` - -The Vite entry point has one default export: - -```ts -function codemodeVitePlugin(): Plugin; -``` - -The plugin appends `export { CodemodeRuntime } from "@cloudflare/codemode"` to the Worker entry module (`src/server.ts`, `src/index.ts`, or `src/worker.ts`). This makes the runtime facet available as `ctx.exports.CodemodeRuntime`, which `createCodemodeRuntime()` requires. - -The plugin leaves the entry module unchanged if it already exports `CodemodeRuntime`. Connector classes need no special file name or import syntax — import them normally and pass instances to the runtime. - -Without the plugin, add the export manually: - -```ts -export { CodemodeRuntime } from "@cloudflare/codemode"; -``` - -A connector import can target one connector file or a directory. A directory import re-exports every matching connector file under that directory. diff --git a/src/content/docs/agents/tools/codemode/browser.mdx b/src/content/docs/agents/tools/codemode/browser.mdx deleted file mode 100644 index 8c3f055191f..00000000000 --- a/src/content/docs/agents/tools/codemode/browser.mdx +++ /dev/null @@ -1,218 +0,0 @@ ---- -title: Browser integration -description: Run model-generated code against browser-owned tools with the Code Mode iframe executor and an Agent chat UI. -pcx_content_type: integration-guide -sidebar: - order: 4 - label: Browser-owned tools -products: - - agents ---- - -import { PackageManagers, Steps, TypeScriptExample } from "~/components"; - -Use `@cloudflare/codemode/browser` when your browser owns the tools that the model must orchestrate. For example, these tools might read page state, access browser APIs, or update data held by your application. - -Code Mode is useful when the model must call several client tools with loops, conditions, or intermediate results. For a single browser action, use a standard client-side tool instead. - -Code Mode presents those tools to the model as typed functions. The model writes one JavaScript async arrow function that can call several tools, combine their results, and apply control flow. `IframeSandboxExecutor` runs that generated code in a sandboxed iframe on the page. - -This integration does not give an agent control of a remote browser. To inspect websites, capture screenshots, or automate pages with the Chrome DevTools Protocol (CDP), refer to [Browser tools](/agents/tools/browser/). - -## Install Code Mode - -Install the package in your client application: - - - -The `@cloudflare/codemode/browser` entry point uses JSON Schema and browser APIs. It does not require the AI SDK or Zod peer dependencies used by `@cloudflare/codemode/ai`. - -## Add Code Mode to an Agent chat UI - -The browser creates the Code Mode tool and registers it as a dynamic client tool. The Agent receives the tool schema, but the tool implementation remains in the browser. - - - -1. Define the browser-owned tools with JSON Schema and an `execute` function. - - - - ```tsx - import type { JsonSchemaExecutableToolDescriptors } from "@cloudflare/codemode/browser"; - - export const browserTools: JsonSchemaExecutableToolDescriptors = { - getPageInfo: { - description: "Get information about the current browser page", - inputSchema: { - type: "object", - properties: {}, - required: [] - }, - execute: async () => ({ - title: document.title, - url: window.location.href - }) - }, - getSelectionText: { - description: "Get the user's current text selection", - inputSchema: { - type: "object", - properties: {}, - required: [] - }, - execute: async () => ({ - text: window.getSelection()?.toString() ?? "" - }) - } - }; - ``` - - - - JSON Schema supplies the types shown to the model. `createBrowserCodeTool()` does not use the schema to validate arguments at runtime. Validate untrusted inputs inside each `execute` function when required. - -2. Create the Code Mode descriptor with an iframe executor. - - - - ```tsx - import { - IframeSandboxExecutor, - createBrowserCodeTool - } from "@cloudflare/codemode/browser"; - import { browserTools } from "./browser-tools"; - - export const codemodeTool = createBrowserCodeTool({ - tools: browserTools, - executor: new IframeSandboxExecutor() - }); - ``` - - - - `createBrowserCodeTool()` returns a plain descriptor named `codemode`. Its description includes generated TypeScript definitions for the browser tools. Its input contains the model-generated JavaScript in a `code` property. - - The `executor` option is optional. When omitted, `createBrowserCodeTool()` creates an `IframeSandboxExecutor` with default settings. - -3. Register the descriptor with `useAgentChat()` and execute client tool calls. - - - - ```tsx - import { useAgentChat, type AITool } from "@cloudflare/ai-chat/react"; - import { useAgent } from "agents/react"; - import { useMemo } from "react"; - import { codemodeTool } from "./codemode-tool"; - - function BrowserCodeModeChat() { - const agent = useAgent({ agent: "browser-codemode" }); - - const tools = useMemo>( - () => ({ - codemode: { - description: codemodeTool.description, - parameters: codemodeTool.inputSchema, - execute: (input) => - codemodeTool.execute(input as { code: string }) - } - }), - [] - ); - - const { messages, sendMessage } = useAgentChat({ - agent, - tools, - onToolCall: async ({ toolCall, addToolOutput }) => { - const tool = tools[toolCall.toolName]; - if (!tool?.execute) return; - - try { - const output = await tool.execute(toolCall.input); - addToolOutput({ - toolCallId: toolCall.toolCallId, - output - }); - } catch (error) { - addToolOutput({ - toolCallId: toolCall.toolCallId, - state: "output-error", - errorText: error instanceof Error ? error.message : String(error) - }); - } - } - }); - - // Render messages and call sendMessage() from your chat UI. - } - ``` - - - - `useAgentChat()` sends the registered client tool schema to the Agent. When the model calls `codemode`, `onToolCall` executes the descriptor in the browser and adds its output to the conversation. - -4. On the Agent, convert the client schemas into model tools. - - - - ```ts - import { AIChatAgent, createToolsFromClientSchemas } from "@cloudflare/ai-chat"; - import { convertToModelMessages, stepCountIs, streamText } from "ai"; - import { createAI } from "agents/models/ai-sdk"; - - export class BrowserCodemode extends AIChatAgent { - async onChatMessage( - _onFinish?: unknown, - options?: { - clientTools?: Parameters[0]; - } - ) { - const ai = createAI({ binding: this.env.AI }); - - const result = streamText({ - model: ai("@cf/moonshotai/kimi-k2.7-code"), - system: - "Use the codemode tool to write JavaScript that calls browser-provided tools.", - messages: await convertToModelMessages(this.messages), - tools: createToolsFromClientSchemas(options?.clientTools), - stopWhen: stepCountIs(10) - }); - - return result.toUIMessageStreamResponse(); - } - } - ``` - - - - The Agent advertises the client-provided schema to the model. It does not run the generated code or the browser tool implementations. - - - -If your browser tool set changes at runtime, create a new Code Mode descriptor and register the updated descriptor with your client tool layer. - -## Iframe execution and security - -`IframeSandboxExecutor` creates a hidden iframe for each execution. The iframe uses `sandbox="allow-scripts"` and receives the generated code through `postMessage`. Tool calls return to the parent page, which runs the matching browser-owned `execute` function. - -Messages are scoped to the current iframe and an execution nonce. The executor removes the iframe and message listener after completion, failure, or timeout. - -The executor accepts these options: - -| Option | Type | Default | Behavior | -| --------- | -------- | --------------------------------------------------------------- | --------------------------------------------------------------- | -| `timeout` | `number` | `30000` | Ends an execution after the specified number of milliseconds. | -| `csp` | `string` | `default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval';` | Sets the Content Security Policy (CSP) for the iframe document. | - -The default CSP blocks resources except the inline and evaluated scripts required to execute generated code. Pass a custom policy only when your generated code needs additional iframe capabilities. - -Relaxing directives such as `connect-src`, `img-src`, or `form-action` can let generated iframe code communicate with external systems. That code could expose values returned by browser tools. Keep outbound destinations narrow, and do not place secrets in tool results. Browser-owned tools execute separately in the parent page with the capabilities their implementations provide. - -:::caution -The timeout cannot interrupt a tight synchronous loop such as `while (true) {}`. That code blocks the browser event loop, so the timeout callback cannot run. Browser-owned tools also execute in the parent page and retain the capabilities you give them. -::: - -## Approval constraints - -`createBrowserCodeTool()` excludes any tool whose `needsApproval` value is `true` or a function. Code Mode does not pause iframe execution to request approval for those tools. - -Keep approval-gated actions outside the Code Mode descriptor. Register them as standard tools and use the [`useAgentChat()` approval flow](/agents/agent-class/communication-channels/chat/chat-agents/#tool-approval-human-in-the-loop) instead. diff --git a/src/content/docs/agents/tools/codemode/durable-runtime.mdx b/src/content/docs/agents/tools/codemode/durable-runtime.mdx deleted file mode 100644 index ca1cd774f18..00000000000 --- a/src/content/docs/agents/tools/codemode/durable-runtime.mdx +++ /dev/null @@ -1,277 +0,0 @@ ---- -title: Create a durable Code Mode runtime -description: Create a Code Mode runtime with connectors, direct host APIs, durable approvals, rollback, execution history, and reusable snippets. -pcx_content_type: how-to -sidebar: - order: 1 -products: - - agents ---- - -import { - PackageManagers, - Steps, - TypeScriptExample, - WranglerConfig, -} from "~/components"; - -This guide adds a durable Code Mode runtime to an Agents SDK application. The runtime stores execution history, pending approvals, and snippets across Durable Object hibernation. - -:::caution -Code Mode is experimental and may introduce breaking changes. Use caution in production. -::: - -## Prerequisites - -You need an existing Agents SDK application with a Durable Object and Vite. The example uses `AIChatAgent` and the AI SDK. - -## Integrate Code Mode - - - -1. Install the Code Mode package: - - - -2. Add a Worker Loader binding. `DynamicWorkerExecutor` uses this binding to run model-generated code in isolated Workers: - - - - ```toml - compatibility_date = "$today" - compatibility_flags = ["nodejs_compat"] - - [[worker_loaders]] - binding = "LOADER" - ``` - - - -3. Add the Agents and Code Mode plugins to `vite.config.ts`: - - - - ```ts - import { cloudflare } from "@cloudflare/vite-plugin"; - import codemode from "@cloudflare/codemode/vite"; - import agents from "agents/vite"; - import { defineConfig } from "vite"; - - export default defineConfig({ - plugins: [agents(), codemode(), cloudflare()], - }); - ``` - - - - The plugin exports the `CodemodeRuntime` facet class from your Worker entry module. The runtime stores execution state in a Durable Object facet, and the Workers runtime requires facet classes to be available through `ctx.exports`. If you do not use the plugin, add the export manually: - - ```ts - export { CodemodeRuntime } from "@cloudflare/codemode"; - ``` - -4. Create a connector. Connectors are plain classes — they need no special file name or import syntax. This example stores notes in the Agent's Durable Object storage: - - - - ```ts - import { - CodemodeConnector, - type ConnectorTools, - } from "@cloudflare/codemode"; - - type Note = { id: string; text: string }; - - export class NotesConnector extends CodemodeConnector { - private storage: DurableObjectStorage; - - constructor(ctx: DurableObjectState, env: Env) { - super(ctx, env); - this.storage = ctx.storage; - } - - override name() { - return "notes"; - } - - protected override instructions() { - return "Use this connector to list and create saved notes."; - } - - protected override tools(): ConnectorTools { - return { - listNotes: { - description: "List saved notes.", - execute: async () => - (await this.storage.get("notes")) ?? [], - }, - createNote: { - description: "Create a saved note.", - inputSchema: { - type: "object", - properties: { text: { type: "string" } }, - required: ["text"], - }, - requiresApproval: true, - execute: async (input) => { - const { text } = input as { text: string }; - const note = { id: crypto.randomUUID(), text }; - const notes = (await this.storage.get("notes")) ?? []; - await this.storage.put("notes", [...notes, note]); - return note; - }, - revert: async (_input, result) => { - const { id } = result as Note; - const notes = (await this.storage.get("notes")) ?? []; - await this.storage.put( - "notes", - notes.filter((note) => note.id !== id), - ); - }, - }, - }; - } - } - ``` - - - - The `name()` result becomes the sandbox global, in this case `notes`. `requiresApproval: true` pauses before `createNote` executes. The optional `revert` function lets `runtime.rollback()` compensate for an applied call. - - Use `McpConnector` for MCP tools or `OpenApiConnector` for OpenAPI operations. For MCP-specific setup, refer to [Use MCP tools with Code Mode](/agents/tools/codemode/mcp/). - -5. Import the connector and create a runtime in your Agent: - - - - ```ts - import { AIChatAgent } from "@cloudflare/ai-chat"; - import { - createCodemodeRuntime, - DynamicWorkerExecutor, - type CodemodeRuntimeHandle, - type ExecutionState, - type PendingAction, - type Snippet, - } from "@cloudflare/codemode"; - import { callable } from "agents"; - import { convertToModelMessages, stepCountIs, streamText } from "ai"; - import { NotesConnector } from "./notes-connector"; - import { model } from "./model"; - - export class Chat extends AIChatAgent { - #runtime(): CodemodeRuntimeHandle { - return createCodemodeRuntime({ - ctx: this.ctx, - executor: new DynamicWorkerExecutor({ loader: this.env.LOADER }), - connectors: [new NotesConnector(this.ctx, this.env)], - }); - } - - async onChatMessage() { - const result = streamText({ - model, - messages: await convertToModelMessages(this.messages), - tools: { codemode: this.#runtime().tool() }, - stopWhen: stepCountIs(10), - }); - - return result.toUIMessageStreamResponse(); - } - - @callable() - async pendingApprovals(): Promise { - return this.#runtime().pending(); - } - - @callable() - async approveExecution(executionId: string) { - return this.#runtime().approve({ executionId }); - } - - @callable() - async rejectExecution(executionId: string, seq: number): Promise { - return this.#runtime().reject({ executionId, seq }); - } - - @callable() - async rollbackExecution(executionId: string): Promise { - await this.#runtime().rollback({ executionId }); - } - - @callable() - async executionHistory(): Promise { - return this.#runtime().executions(20); - } - - @callable() - async saveSnippet( - name: string, - description: string, - executionId: string, - ): Promise { - const runtime = this.#runtime(); - const execution = (await runtime.executions()).find( - (item) => item.id === executionId, - ); - if (execution?.status !== "completed") { - throw new Error("Only completed executions can be saved as snippets."); - } - - return runtime.saveSnippet(name, { description, executionId }); - } - - @callable() - async snippets(): Promise { - return this.#runtime().snippets(); - } - } - ``` - - - - Replace the `model` import with the existing model setup in your application. - - - -## Use the runtime without the AI SDK - -Use `execute()`, `search()`, and `describe()` when an MCP server or another host invokes Code Mode without an AI SDK tool adapter: - - - -```ts -const runtime = this.#runtime(); -const matches = await runtime.search("create note"); -const method = matches.results[0]; -const docs = await runtime.describe(method.path); -const outcome = await runtime.execute({ - code: `async () => notes.createNote({ text: "Follow up" })`, -}); -``` - - - -`search()` and `describe()` do not run sandbox code. Their results include `requiresApproval: true` for connector methods that pause before execution. - -`execute()` returns the same durable result as the model-facing tool. A result can complete, pause, or contain an execution error. Resolve a paused result with `approve()` or `reject()`. - -## Verify the integration - -Ask the model to list saved notes. The model receives one `codemode` tool and can discover connector methods inside the sandbox: - -```js -async () => { - const matches = await codemode.search("list saved notes"); - const docs = await codemode.describe(matches.results[0].path); - const savedNotes = await notes.listNotes(); - - return { docs, savedNotes }; -}; -``` - -When the model calls `notes.createNote()`, the execution pauses. Use `pendingApprovals()` to show the pending action. Pass its `executionId` to `approveExecution()`, or pass both `executionId` and `seq` to `rejectExecution()`. - -Approval resumes the same script through replay. Completed calls return recorded results instead of running again. Rejection ends the paused execution without undoing earlier actions. - -Call `rollbackExecution()` to compensate for applied calls whose currently configured connector provides `revert`. Save only completed executions as snippets. diff --git a/src/content/docs/agents/tools/codemode/how-it-works.mdx b/src/content/docs/agents/tools/codemode/how-it-works.mdx deleted file mode 100644 index 228f1c7c606..00000000000 --- a/src/content/docs/agents/tools/codemode/how-it-works.mdx +++ /dev/null @@ -1,313 +0,0 @@ ---- -title: How Code Mode works -pcx_content_type: concept -description: Learn how Code Mode isolates generated code and makes approvals, replay, rollback, and reuse durable. -sidebar: - order: 7 -products: - - agents -tags: - - AI ---- - -import { TypeScriptExample } from "~/components"; - -Code Mode is a pattern where a model writes code to compose tools. The `@cloudflare/codemode` package implements that pattern with an isolated executor, service connectors, and a durable runtime. - -These parts have separate responsibilities. The executor runs code but stores no state. Connectors provide capabilities but do not manage replay. The runtime records execution and controls approvals, replay, rollback, and reuse. - -:::caution -Code Mode is experimental and may have breaking changes. -::: - -## What the model sees - -In the standard setup, the model receives one outer tool named `codemode`. That tool accepts one field and returns a durable execution outcome: - -```ts -type CodeModeInput = { - code: string; -}; - -type PendingAction = { - executionId: string; - seq: number; - connector: string; - method: string; - args: unknown; -}; - -type CodeModeOutput = - | { status: "completed"; executionId: string; result: unknown; logs?: string[] } - | { status: "paused"; executionId: string; pending: PendingAction[] } - | { status: "error"; executionId: string; error: string; logs?: string[] }; -``` - -Its description tells the model to write a JavaScript async arrow function. The description lists the configured connector namespace names, such as `github` or `stripe`, but it does not include every connector method and schema. - -The model can use one Code Mode execution to discover relevant methods, then use the returned paths and types in its next execution. This keeps the complete tool catalog out of the initial model context. - -### Platform SDK - -Inside the sandbox, the `codemode` global provides the platform-level SDK: - -```ts -declare const codemode: { - search(query: string): Promise; - describe(target: string): Promise; - step(name: string, fn: () => T | Promise): Promise; - run(name: string, input?: unknown): Promise; -}; - -type SearchOutput = { - results: Array<{ - path: string; - connector: string; - method: string; - description?: string; - kind: "method" | "snippet"; - score: number; - }>; - total: number; - truncated: boolean; -}; - -type DescribeOutput = { - path: string; - description?: string; - types: string; - kind: "connector" | "method" | "snippet"; -}; -``` - -`codemode.search()` searches connector methods and saved snippets. It returns ranked paths, not complete schemas. The model can then pass one path to `codemode.describe()` to request focused TypeScript documentation. - -`codemode.step()` records nondeterministic or side-effectful sandbox work for replay. `codemode.run()` invokes a saved snippet. - -### Connector SDKs - -Each configured connector becomes another sandbox global. A connector named `github` is available as `github`, and its methods appear under paths such as `github.list_pull_requests`. - -A connector-level description returns declarations similar to: - -```ts -type ListPullRequestsInput = { - owner: string; - repo: string; - state?: "open" | "closed"; -}; - -type ListPullRequestsOutput = unknown; - -declare const github: { - list_pull_requests( - input: ListPullRequestsInput, - ): Promise; -}; -``` - -These declarations are generated from connector schemas. They are illustrative; the actual method names, input fields, and output types depend on the connector. - -The sandbox also includes standard JavaScript globals. It does not expose Node.js APIs, host credentials, `process`, `require`, or unrestricted network access. All external operations go through connector globals unless the executor explicitly provides another capability. - -## Executor, connectors, and runtime - -### Executor - -An executor runs one block of model-generated code once. It receives callable namespaces and returns a result, an error, and captured console output. It does not retain execution history. - -`DynamicWorkerExecutor` uses a [Dynamic Worker Loader](/dynamic-workers/) to create an isolated Worker for each execution pass. A resumed execution runs the code again in another pass. Durable state therefore cannot live inside the sandbox. - -External `fetch()` and `connect()` calls are blocked by default. `DynamicWorkerExecutor` configures `globalOutbound: null` unless you provide another value. You can provide a `Fetcher` to route outbound requests through a controlled service. - -### Connectors - -Connectors bridge host-side services into the sandbox. A connector can wrap a Model Context Protocol (MCP) server, an OpenAPI document, an AI SDK toolset, or custom code. - -Each connector becomes a global namespace. For example, a connector named `github` exposes calls such as `github.list_pull_requests()`. The generated code never receives the connector credentials or client objects. - -Connector calls cross the sandbox boundary through [Workers remote procedure calls (RPC)](/workers/runtime-apis/rpc/). The runtime intercepts each call before the connector executes it. This interception applies approval, logging, replay, and rollback policy. - -The `codemode` global provides discovery and runtime operations. `codemode.search()` finds connector methods and saved snippets. `codemode.describe()` returns focused TypeScript documentation without placing every connector schema in the model context. - -### Durable runtime - -The runtime connects the executor and connectors. It stores execution records, connector-call logs, pending approvals, and snippets in isolated SQLite storage. This state survives request completion and Durable Object hibernation. - -The executor and connector instances remain transient. Your application provides them again when it handles a later approval or request. - -A typical Agent creates all three parts together: - - - -```ts -import { - createCodemodeRuntime, - DynamicWorkerExecutor, -} from "@cloudflare/codemode"; - -const runtime = createCodemodeRuntime({ - ctx: this.ctx, - executor: new DynamicWorkerExecutor({ loader: this.env.LOADER }), - connectors: [github, repoApi], -}); - -const tools = { codemode: runtime.tool() }; -``` - - - -Code Mode stores this state in a Durable Object facet. A facet is a durable child of the Agent with its own SQLite storage. `createCodemodeRuntime()` and the Vite plugin manage this implementation detail. You do not create or address the facet directly. - -## Use multiple runtimes - -Most Agents need only one Code Mode runtime. If you omit `name`, the runtime uses the name `default`. - -Set `name` when one Agent needs separate Code Mode histories. For example, a runtime named `research` and another named `operations` keep separate execution records and snippet collections: - - - -```ts -const researchRuntime = createCodemodeRuntime({ - ctx: this.ctx, - executor, - connectors: researchConnectors, - name: "research", -}); - -const operationsRuntime = createCodemodeRuntime({ - ctx: this.ctx, - executor, - connectors: operationsConnectors, - name: "operations", -}); -``` - - - -A runtime name identifies its durable storage. It does not name the model, connector, tool, or individual execution. - -Changing the connector set does not create another runtime. Each execution records every connector configured when it starts, and a saved snippet inherits that list. Approval replay and snippet execution require all recorded connectors to remain available, even if the original code did not call each one. - -## Durable execution log - -The runtime assigns each execution a stable ID. Each connector call and `codemode.step()` entry receives a sequence number. The log records its arguments, state, replay policy, and result when applicable. - -The runtime marks a call as `executing` before invoking its connector. It marks the call as `applied` after recording the result. If the host stops before completing the pass, the execution can remain in `running`. A later `expirePaused()` maintenance call marks that stale execution as an error and releases its resources. Approval does not resume a stale `running` execution. - -This log is the replay spine. It also supports developer audit views and determines which actions can be rolled back. It is not general conversation memory and does not replace Agent state. - -## Approvals through abort and replay - -A connector method can require user approval. When generated code reaches that method, the runtime records the action as pending and aborts the current pass. The action does not receive a provisional result. - -The application can show the pending method and arguments to a user. Approval starts another pass with the same source code and execution ID. Calls already marked as applied return their recorded results instead of executing again. The approved action then executes, and the code continues until completion or another approval. - -```txt -first pass: read ── execute ──> result - write ── pause - -approval - -second pass: read ── replay ───> recorded result - write ── execute ─> result - next call ────────> continue -``` - -This design lets an approval wait beyond a request or hibernation. Generated code remains linear and does not implement pause or resume logic. - -Only a paused execution can resume. A stale approval cannot revive a completed, rejected, or rolled-back execution. Rejecting an action ends the execution, but it does not undo earlier actions. Rollback is a separate operation. - -Execution failures are returned as data to the agent loop. Sandbox errors and replay divergence therefore do not need to escape as uncaught RPC exceptions. - -## Deterministic replay - -Replay requires connector calls and steps to occur in the same order. On every pass, a given sequence number must use the same connector, method, and arguments. A mismatch ends the execution with a replay-divergence error. - -Recorded connector results make normal data-dependent branches stable. However, values from `Date.now()`, `Math.random()`, or other nondeterministic sources can change control flow or action arguments. - -Use `codemode.step()` to capture such work once. The runtime records the closure result and returns that value during approval replay: - -```js -async () => { - const createdAt = await codemode.step("created-at", () => Date.now()); - - return github.create_issue({ - owner: "cloudflare", - repo: "agents", - title: `Review created at ${createdAt}`, - }); -}; -``` - -Connector calls already pass through the runtime and do not need a step wrapper. Use steps for nondeterministic or side-effectful work outside connector calls. If you explicitly allow direct network access, this includes direct network operations that must not repeat during approval replay. - -Issue connector calls sequentially when an execution might pause. The host assigns sequence numbers when calls arrive. Calls in `Promise.all()` can arrive in different orders across passes and cause replay divergence. - -## Resource lifetimes - -Some connectors need resources beyond one method call. Examples include browser sessions, database transactions, and temporary workspaces. Connector methods receive the stable execution ID, which can key durable resource metadata across passes. - -Code Mode distinguishes two resource lifetimes: - -- **Pass resources** last for one sandbox pass. The runtime invokes `onPassEnd()` after completed, failed, and paused passes. -- **Execution resources** last for the whole execution. The runtime invokes `disposeExecution()` after completion, failure, rejection, or rollback, but not after a pause. - -A paused execution can resume in another Worker invocation. Connector lifecycle hooks must not depend on instance memory. Cleanup must also be idempotent because a completed execution can later be rolled back and disposed again. - -The runtime calls lifecycle hooks for every configured connector. A connector that did not allocate a resource should safely do nothing. Cleanup errors are ignored so they do not turn a finished execution into a failed one. - -## Replay policy - -By default, the runtime stores a connector result and replays it on later passes. This preserves the exact value that the original code observed. - -A connector can mark a call with `replay: "reexecute"`. The runtime still logs its sequence and arguments, but it does not store the result. A later pass runs the connector method again. - -Use this policy only for idempotent reads with large, inexpensive results. The result can change between passes, so generated code must tolerate that change. Approval-required methods cannot use `replay: "reexecute"` because replay could apply an approved side effect more than once. - -## Rollback - -Rollback walks applied connector calls in reverse order. It invokes the `revert` implementation for every applied method that provides one, regardless of whether that method required approval. - -For each applied connector call, the runtime asks the currently configured connector to run its `revert` implementation. Methods without `revert` remain applied. Missing connectors are also skipped. A failed revert does not stop later compensation attempts, and the runtime reports failures after trying the remaining calls. The execution moves to `rolled_back` only if at least one call is reverted. - -Rollback is compensation, not database transaction isolation. Connector authors define what reversal means for each action. An external system may also change between the original call and its compensation. - -## Retention and stale executions - -The execution log is an audit trail and grows over time. When a new run begins, the runtime first inserts that run and then prunes older terminal executions. `maxExecutions` defaults to 50. Because a running execution is not terminal, completion can temporarily leave 51 terminal records until another run begins or you call `pruneExecutions()`. - -Running and paused executions are not pruned automatically. They may still need to finish or resume. Use `expirePaused()` from recurring maintenance to reclaim stale nonterminal runs. The runtime marks stale paused runs as rejected and stale running runs as errors, then disposes their execution resources. - -You can also remove individual execution records or prune terminal history explicitly. Deleting a nonterminal execution disposes its execution-scoped resources. - -## Durable value and result limits - -Each value stored for durable replay has a serialized character limit of 1,000,000. The implementation checks the JavaScript string length after serialization. This limit applies to connector arguments, recorded connector results, step results, and execution source code. - -The runtime cannot truncate these values. Truncation would provide different data during replay. An oversized or unserializable replay value therefore fails the execution and suggests storing the data elsewhere, then passing a small reference such as a file path. - -A final result has different behavior because replay does not consume it. The execution can complete and return the real result to the model. If the result cannot fit in the audit record, the runtime stores an omission message there instead. - -`transformResult` can reshape the completed result before the model receives it. The transform runs after the runtime attempts to record the raw result. The audit trail retains the original value when it fits, while the model can receive a smaller representation. - -## Snippets - -A snippet is saved source from an execution. Snippets turn model-written programs into reusable recipes. They remain available across requests and hibernation. - -The model does not promote its own code. Your application reviews an execution and calls `runtime.saveSnippet()` with its execution ID. The API accepts any execution status, so verify that the execution completed successfully before saving it. The model can then find the snippet with `codemode.search()`, inspect it with `codemode.describe()`, and invoke it with `codemode.run()`. - - - -```ts -const runs = await runtime.executions(20); - -await runtime.saveSnippet("list-open-prs", { - executionId: runs[0].id, - description: "List open pull requests for a repository.", -}); -``` - - - -A snippet can accept an input value. Its connector calls join the current execution log when the model runs it. The snippet also retains the connector list from its source execution. If a recorded connector is unavailable, `codemode.run()` resolves to an object with an `error` property. It does not throw automatically. diff --git a/src/content/docs/agents/tools/codemode/index.mdx b/src/content/docs/agents/tools/codemode/index.mdx deleted file mode 100644 index aecca29ccee..00000000000 --- a/src/content/docs/agents/tools/codemode/index.mdx +++ /dev/null @@ -1,106 +0,0 @@ ---- -title: Code Mode -description: Use Code Mode to let models discover and compose tools by writing code as a compact, executable plan. -pcx_content_type: overview -sidebar: - order: 6 - group: - hideIndex: false -products: - - agents ---- - -import { CardGrid, LinkCard } from "~/components"; - -Code Mode is a tool-use pattern where a model writes code instead of requesting each operation separately. The model receives one code-execution tool. Its code becomes a compact plan that calls tools, processes results, and returns the information needed for a response. - -Code Mode exposes configured tools as typed methods. Models use those methods to express multi-step work with familiar programming constructs. Depending on the integration, tool definitions can be provided up front or discovered when needed. - -:::note -Code Mode is experimental and may introduce breaking changes. Use caution in production. -::: - -## Code as a plan - -With direct tool use, the model selects one tool, receives its result, and then decides what to do next. Each operation can require another round trip through the model. - -Code Mode moves that intermediate logic into executable code. A single plan can: - -- Compose several dependent tool calls. -- Loop over collections of results. -- Filter and transform returned data. -- Branch based on earlier results. -- Shape the final returned value. - -This approach keeps control flow and data handling together. It is useful when the model must coordinate several operations before producing an answer. - -## Progressive tool discovery - -Large tool catalogs can consume significant model context if every definition is loaded up front. Connector-based Code Mode runtimes support progressive discovery through `codemode.search()` and `codemode.describe()`. - -Search finds relevant connectors, methods, and saved snippets. Describe returns detailed type information for a selected target. These results enter the running code, so the model can pull the definitions it needs instead of receiving the entire catalog with every request. - -## Choose between Code Mode and direct tool calls - -With direct tool calls, each result returns to the model before it chooses the next operation. Intermediate data consumes context, even when the model only needs a small part of it for the final answer. - -Code Mode keeps that work inside one sandbox execution. Generated code can pass results between tools, filter intermediate data, and return only the final value the model needs. This makes multi-tool tasks more efficient and easier to generalize or repeat. - -Use direct tool calls for simple tasks with a small, fixed tool set. Use Code Mode when a task needs composition, dependent calls, progressive discovery, reusable logic, or control flow: - -| Pattern | Best suited for | -| ----------------- | ---------------------------------------------------------------------------------------------------------------- | -| Direct tool calls | Simple tasks using a small, known tool set | -| Code Mode | Composed or dependent calls, large tool catalogs, loops, branching, filtering, result shaping, or reusable logic | - -## Choose an integration - -Code Mode provides surfaces for agent runtimes, AI frameworks, browsers, and Model Context Protocol (MCP) systems. - - - - - - - - - - -## Understand the pattern - - - - - diff --git a/src/content/docs/agents/tools/codemode/mcp.mdx b/src/content/docs/agents/tools/codemode/mcp.mdx deleted file mode 100644 index a12ef48cca2..00000000000 --- a/src/content/docs/agents/tools/codemode/mcp.mdx +++ /dev/null @@ -1,204 +0,0 @@ ---- -title: Use MCP tools with Code Mode -description: Expose tools from an existing MCP connection to models through a durable Code Mode runtime. -pcx_content_type: how-to -sidebar: - order: 5 - label: MCP -products: - - agents ---- - -import { PackageManagers, Steps, TypeScriptExample } from "~/components"; - -Use `McpConnector` to expose tools from an existing Model Context Protocol (MCP) client connection inside the Code Mode sandbox. The connector works with the durable runtime, including discovery, approvals, and execution history. - -This page covers an Agent consuming an MCP server. To publish Code Mode as an MCP server, refer to [Build a Code Mode MCP server](/agents/model-context-protocol/guides/build-codemode-mcp-server/). - -## Prerequisites - -You need: - -- A project with the [durable Code Mode runtime](/agents/tools/codemode/durable-runtime/) configured. That setup provides the Worker Loader binding and the `CodemodeRuntime` export. -- An existing Agents SDK MCP connection. To create and authorize the connection, refer to the [MCP Client](/agents/model-context-protocol/apis/client-api/). - - - -1. **Install Code Mode** - - If your project does not already include Code Mode, install `@cloudflare/codemode`: - - - -2. **Create an MCP connector** - - Create the connector in its own file. It is a plain class with no special file name or import syntax. - - - - ```ts - import { - McpConnector, - type ConnectorTool, - type McpConnectionLike, - } from "@cloudflare/codemode"; - - export class GithubConnector extends McpConnector { - private connection: McpConnectionLike; - - constructor( - ctx: DurableObjectState | ExecutionContext, - env: Env, - connection: McpConnectionLike, - ) { - super(ctx, env); - this.connection = connection; - } - - override name() { - return "github"; - } - - protected override instructions() { - return "Use for GitHub repositories, issues, and pull requests."; - } - - protected override createConnection() { - return this.connection; - } - - protected override tool( - name: string, - tool: ConnectorTool, - ): ConnectorTool { - if (name === "create_issue") { - return { ...tool, requiresApproval: true }; - } - - return tool; - } - } - ``` - - - - `createConnection()` returns the existing Agents SDK connection. `name()` defines the sandbox global, so this connector exposes methods under `github`. Use a unique connector name within each runtime. - - `McpConnector` creates one typed sandbox method for each discovered MCP tool. It derives the method types from the MCP schemas. Each method calls the original tool through `connection.client.callTool()`. - - The connector sanitizes MCP tool names into valid JavaScript identifiers. For example, `list-pull.requests` becomes `list_pull_requests`, `3d-render` becomes `_3d_render`, and `delete` becomes `delete_`. If two source names produce the same identifier, the connector throws an error. Override `toolName()` to disambiguate those tools. - - The `tool()` decoration hook receives each generated method by its sanitized name. In this example, the hook marks `create_issue` for approval. The durable runtime pauses before executing that method and resumes the run after approval. - -3. **Add the connector to the runtime** - - In your Agent, find the existing MCP connection and pass it to the connector. Then include the connector when you create the Code Mode runtime: - - - - ```ts - import { Agent } from "agents"; - import { - createCodemodeRuntime, - DynamicWorkerExecutor, - } from "@cloudflare/codemode"; - import { GithubConnector } from "./github-connector"; - - export class Chat extends Agent { - private async codemodeRuntime() { - await this.mcp.waitForConnections(); - - const server = this.mcp - .listServers() - .find((server) => server.name === "github"); - - if (!server) { - throw new Error("GitHub MCP server is not registered."); - } - - const connection = this.mcp.mcpConnections[server.id]; - if (!connection) { - throw new Error("GitHub MCP connection is not available."); - } - - return createCodemodeRuntime({ - ctx: this.ctx, - executor: new DynamicWorkerExecutor({ loader: this.env.LOADER }), - connectors: [ - new GithubConnector(this.ctx, this.env, connection), - ], - }); - } - } - ``` - - - - Await `codemodeRuntime()`, then pass `runtime.tool()` to your model as the `codemode` tool. Await the helper again before calling approval, rejection, rollback, or snippet methods. This ensures MCP connections have finished restoring after hibernation. - - If the runtime belongs to a `Think` agent, keep MCP tools out of the direct model tool set: - - - - ```ts - import { Think } from "@cloudflare/think"; - - export class Chat extends Think { - includeMcpTools = false; - waitForMcpConnections = true; - } - ``` - - - - `includeMcpTools = false` skips Think's automatic `getAITools()` call. The MCP connection remains available to `McpConnector`. - -4. **Let the model discover and call tools** - - Tell the model to use `codemode.search()` and `codemode.describe()` before calling unfamiliar methods. Model-generated sandbox code can then discover and call the generated methods: - - ```js - async () => { - const matches = await codemode.search("open pull requests"); - const docs = await codemode.describe(matches.results[0].path); - - const pullRequests = await github.list_pull_requests({ - owner: "cloudflare", - repo: "agents", - state: "open", - }); - - return { docs, pullRequests }; - }; - ``` - - `codemode.search()` returns ranked connector methods. `codemode.describe()` returns TypeScript documentation for a connector or method. This lets the model load tool details only when needed. - - - -When the model calls `github.create_issue()`, the runtime returns a paused execution. Approve that execution through the runtime to execute the MCP tool and continue the same sandbox program. - -## Use an AI SDK tool collection - -For a smaller integration without durable approvals or `codemode.search()` and `codemode.describe()`, pass the Agents SDK tool collection directly to `createCodeTool()`: - - - -```ts -import { DynamicWorkerExecutor } from "@cloudflare/codemode"; -import { createCodeTool } from "@cloudflare/codemode/ai"; - -await this.mcp.waitForConnections(); - -const executor = new DynamicWorkerExecutor({ loader: this.env.LOADER }); -const codemode = createCodeTool({ - tools: this.mcp.getAITools(), - executor, -}); -``` - - - -This approach exposes the MCP tools under the default `codemode` namespace. It does not use the connector runtime's durable pause, approval, and resume flow. Use `McpConnector` when tools can cause side effects or when the model needs on-demand discovery. - -`getAITools()` converts MCP input and output schemas for use by the AI SDK. The Agents SDK reuses those converted schemas while each live connection keeps the same current catalog. Use `this.mcp.listTools()` instead when you only need to inspect the raw MCP catalog. diff --git a/src/content/docs/agents/tools/codemode/openapi.mdx b/src/content/docs/agents/tools/codemode/openapi.mdx deleted file mode 100644 index da3f56d0ebc..00000000000 --- a/src/content/docs/agents/tools/codemode/openapi.mdx +++ /dev/null @@ -1,227 +0,0 @@ ---- -title: Use an OpenAPI service with Code Mode -description: Turn OpenAPI operations into typed Code Mode connector methods while keeping authentication in the host Worker. -pcx_content_type: how-to -sidebar: - order: 6 - label: OpenAPI -products: - - agents ---- - -import { Steps, TypeScriptExample } from "~/components"; - -Use `OpenApiConnector` to expose an OpenAPI service inside a durable Code Mode runtime. The connector derives one sandbox method for each operation in the OpenAPI document. - -The model can discover methods with `codemode.search()` and request focused input types with `codemode.describe()`. The complete OpenAPI document does not need to enter the model context. - -This page covers an Agent consuming an OpenAPI service. To publish an OpenAPI service to external MCP clients through `search` and `execute`, refer to [Build a Code Mode MCP server](/agents/model-context-protocol/guides/build-codemode-mcp-server/#publish-a-large-api-with-search-and-execute). - -## Prerequisites - -You need a project with the [durable Code Mode runtime](/agents/tools/codemode/durable-runtime/) configured. The runtime setup provides the Worker Loader binding and the `CodemodeRuntime` export used in this guide. - -## Create an OpenAPI connector - - - -1. Add the OpenAPI document to your project. Give each operation a unique `operationId` so it produces a stable sandbox method name: - - - - ```ts - export const ordersOpenApiSpec = { - openapi: "3.1.0", - info: { title: "Orders API", version: "1.0.0" }, - paths: { - "/orders/{orderId}": { - get: { - operationId: "get_order", - summary: "Get an order by ID.", - parameters: [ - { - name: "orderId", - in: "path", - required: true, - schema: { type: "string" }, - }, - ], - }, - }, - "/orders": { - post: { - operationId: "create_order", - summary: "Create an order.", - requestBody: { - required: true, - content: { - "application/json": { - schema: { - type: "object", - properties: { - productId: { type: "string" }, - quantity: { type: "integer" }, - }, - required: ["productId", "quantity"], - }, - }, - }, - }, - }, - }, - }, - } as const; - ``` - - - -2. Create the connector. Implement `spec()` to return the document and `request()` to make authenticated host-side requests: - - - - ```ts - import { - OpenApiConnector, - type ConnectorTool, - type OpenApiRequestOptions, - } from "@cloudflare/codemode"; - import { ordersOpenApiSpec } from "./orders-openapi"; - - const API_ORIGIN = "https://api.example.com"; - - export class OrdersConnector extends OpenApiConnector { - override name() { - return "orders"; - } - - protected override instructions() { - return "Use for reading and creating orders."; - } - - protected override spec() { - return ordersOpenApiSpec; - } - - protected override async request(options: OpenApiRequestOptions) { - if (!options.path.startsWith("/")) { - throw new Error("Orders API path must start with a slash"); - } - - const url = new URL(options.path, API_ORIGIN); - for (const [key, value] of Object.entries(options.params ?? {})) { - if (value !== undefined) { - url.searchParams.set(key, String(value)); - } - } - - const response = await fetch(url, { - method: options.method ?? "GET", - headers: { - ...(options.body !== undefined - ? { "Content-Type": "application/json" } - : {}), - ...options.headers, - Authorization: `Bearer ${this.env.ORDERS_API_TOKEN}`, - }, - body: - options.body === undefined - ? undefined - : JSON.stringify(options.body), - }); - - if (!response.ok) { - throw new Error(`Orders API request failed: ${response.status}`); - } - if (response.status === 204) return null; - return response.json(); - } - - protected override tool(name: string, tool: ConnectorTool): ConnectorTool { - if (name === "create_order") { - return { ...tool, requiresApproval: true }; - } - return tool; - } - } - ``` - - - - Credentials remain in the host Worker. Model-written code receives connector methods and their results, not `ORDERS_API_TOKEN`. - - The `tool()` hook decorates derived operations. This example requires approval before `create_order` executes. You can also use the hook to add replay or rollback behavior. - -3. Import the connector and add it to the runtime: - - - - ```ts - import { AIChatAgent } from "@cloudflare/ai-chat"; - import { - createCodemodeRuntime, - DynamicWorkerExecutor, - } from "@cloudflare/codemode"; - import { OrdersConnector } from "./orders-connector"; - - export class Chat extends AIChatAgent { - #runtime() { - return createCodemodeRuntime({ - ctx: this.ctx, - executor: new DynamicWorkerExecutor({ loader: this.env.LOADER }), - connectors: [new OrdersConnector(this.ctx, this.env)], - }); - } - - async onChatMessage() { - const tools = { codemode: this.#runtime().tool() }; - // Pass tools to your model call. - } - } - ``` - - - -4. Let the model discover the operations and call the generated connector methods: - - ```js - async () => { - const matches = await codemode.search("get an order by ID"); - const docs = await codemode.describe(matches.results[0].path); - - const order = await orders.get_order({ orderId: "order-123" }); - return { docs, order }; - }; - ``` - - - -## Derived method behavior - -`OpenApiConnector` uses a sanitized `operationId` as the method name. If an operation has no `operationId`, it derives a name from the HTTP method and path. Define unique operation IDs to keep method names stable and avoid collisions. - -Each generated method accepts one object: - -- Path, query, and header parameters appear as top-level fields. -- A JSON request body appears under `body`. -- Required OpenAPI parameters become required TypeScript fields. -- Local `$ref` values in input schemas are resolved before types are generated. - -The connector substitutes path parameters and passes a normalized `{ path, method, params, body, headers }` object to `request()`. - -The current connector derives input types but does not derive response types from OpenAPI response schemas. Generated methods therefore return `unknown` unless your application provides more specific declarations through another connector implementation. - -## Request escape hatch - -Every OpenAPI connector also exposes a low-level `request()` sandbox method. Use it when the OpenAPI document does not describe an operation the model needs: - -```js -const result = await orders.request({ - path: "/orders", - method: "GET", - params: { status: "processing" }, -}); -``` - -Prefer derived operation methods when available. They provide discoverable descriptions and generated input types. - -`exposeSpec()` returns `false` by default. Override it to return `true` only when model-written code needs access to the raw OpenAPI document. Large documents can produce large results and durable log entries. diff --git a/src/content/docs/agents/tools/codemode/tanstack-ai.mdx b/src/content/docs/agents/tools/codemode/tanstack-ai.mdx deleted file mode 100644 index a06c91afcbc..00000000000 --- a/src/content/docs/agents/tools/codemode/tanstack-ai.mdx +++ /dev/null @@ -1,164 +0,0 @@ ---- -title: Use Code Mode with TanStack AI -description: Use @cloudflare/codemode/tanstack-ai to expose namespaced TanStack AI server tools through chat(). -pcx_content_type: how-to -sidebar: - order: 3 -products: - - agents ---- - -import { - PackageManagers, - Steps, - TypeScriptExample, - WranglerConfig, -} from "~/components"; - -Use the `@cloudflare/codemode/tanstack-ai` entry point to give `chat()` one Code Mode tool. The model can then write JavaScript that calls your TanStack AI server tools. - -## Prerequisites - -You need an existing Workers project and a configured TanStack AI model adapter. This example uses the OpenAI adapter. - -## Add Code Mode - - - -1. Install Code Mode, TanStack AI, the OpenAI adapter, and Zod: - - - -2. Add a Worker Loader binding to your Wrangler configuration: - - - - ```toml - name = "tanstack-codemode" - main = "src/index.ts" - upload_source_maps = true - compatibility_date = "$today" - compatibility_flags = ["nodejs_compat"] - - [[worker_loaders]] - binding = "LOADER" - ``` - - - -3. Define TanStack AI server tools, group them into namespaces, and pass the Code Mode tool to `chat()`: - - - - ```ts - import { DynamicWorkerExecutor } from "@cloudflare/codemode"; - import { - createCodeTool, - tanstackTools, - } from "@cloudflare/codemode/tanstack-ai"; - import { chat, toolDefinition, toHttpResponse } from "@tanstack/ai"; - import { openaiText } from "@tanstack/ai-openai"; - import { z } from "zod"; - - const getWeather = toolDefinition({ - name: "get_weather", - description: "Get the current weather for a city", - inputSchema: z.object({ - city: z.string().meta({ description: "City name" }), - }), - outputSchema: z.object({ - city: z.string(), - temperatureCelsius: z.number(), - conditions: z.string(), - }), - }).server(async ({ city }) => ({ - city, - temperatureCelsius: 22, - conditions: "sunny", - })); - - const findContacts = toolDefinition({ - name: "find_contacts", - description: "Find contacts for a team", - inputSchema: z.object({ - team: z.string().meta({ description: "Team name" }), - }), - outputSchema: z.array( - z.object({ - name: z.string(), - email: z.string(), - }), - ), - }).server(async ({ team }) => [ - { - name: `${team} contact`, - email: "team@example.com", - }, - ]); - - function startChat(env: Env, prompt: string) { - const executor = new DynamicWorkerExecutor({ loader: env.LOADER }); - - const codeTool = createCodeTool({ - tools: [ - tanstackTools([getWeather], "weather"), - tanstackTools([findContacts], "directory"), - ], - executor, - }); - - return chat({ - adapter: openaiText("gpt-4o"), - messages: [{ role: "user", content: prompt }], - tools: [codeTool], - }); - } - - export default { - async fetch(request, env): Promise { - const prompt = await request.text(); - return toHttpResponse(startChat(env, prompt)); - }, - } satisfies ExportedHandler; - ``` - - - - - -`createCodeTool()` returns a TanStack AI `ServerTool` named `codemode_execute`. Its description contains the generated types for both namespaces. The model can write code similar to this: - -```js -async () => { - const weatherResult = await weather.get_weather({ city: "London" }); - const contacts = await directory.find_contacts({ team: "travel" }); - return { weatherResult, contacts }; -}; -``` - -## Namespace behavior - -`tanstackTools(tools, name)` converts an array of TanStack AI tools into a Code Mode tool provider. It uses each tool name as the method name and generates types from its input and output schemas. - -The optional second argument sets the sandbox namespace. For example, `tanstackTools([getWeather], "weather")` exposes `weather.get_weather()`. If you omit the name, Code Mode uses the default `codemode` namespace: - - - -```ts -const codeTool = createCodeTool({ - tools: [tanstackTools([getWeather])], - executor, -}); - -// Available to model-generated code as codemode.get_weather(). -``` - - - -Use distinct namespace names when you combine tool groups. Each provider contributes its generated declarations and executable server tools to the same Code Mode tool. - -## Approval behavior - -The `createCodeTool()` integration does not pause execution for TanStack AI approvals. `tanstackTools()` excludes a tool when its `needsApproval` property is `true` or a function. The excluded tool does not appear in generated type declarations and cannot run in the sandbox. - -Tools with `needsApproval: false` remain available. The durable Code Mode runtime supports paused approvals through connector `requiresApproval` annotations, but this `createCodeTool()` integration does not use that approval flow. diff --git a/src/content/docs/agents/tools/filesystem.mdx b/src/content/docs/agents/tools/filesystem.mdx index c95899ea37e..1e6163add14 100644 --- a/src/content/docs/agents/tools/filesystem.mdx +++ b/src/content/docs/agents/tools/filesystem.mdx @@ -439,7 +439,7 @@ const { exitCode, value, stdout, stderr } = await run.result(); ### Give code access to your own functions -Custom modules can be registered under the `ws:` namespace. Use these modules to give the model access to your application logic or file system tools, or to perform external actions. +Custom modules can be registered under the `codemode:` namespace. Use these modules to give the model access to your application logic or file system tools, or to perform external actions. For more ways to supply tools as modules, including MCP tools and HTTP APIs, refer to [Run code with Computer](/agents/runtime/code-mode/computer/). @@ -448,7 +448,7 @@ Custom modules can be registered under the `ws:` namespace. Use these modules to new WorkerJavaScriptBackend({ loader: this.env.LOADER, modules: { - "ws:orders": { + "codemode:orders": { // Arguments arrive as an array of JSON-compatible values. lookup: ([orderId]) => this.lookUpOrder(String(orderId)), }, @@ -461,7 +461,7 @@ new WorkerJavaScriptBackend({ Code imports the module like any other: ```js -import { lookup } from "ws:orders"; +import { lookup } from "codemode:orders"; export default async function ({ ids }) { return Promise.all(ids.map((id) => lookup(id))); @@ -513,7 +513,7 @@ new WorkerJavaScriptBackend({ loader: this.env.LOADER, onInterrupt: "replay", modules: { - "ws:billing": { + "codemode:billing": { refund: ([order], { callId }) => this.refund(String(order), { idempotencyKey: callId }), charge: unsafeToRepeat(([order, cents]) => @@ -537,7 +537,7 @@ new WorkerJavaScriptBackend({ loader: this.env.LOADER, onInterrupt: "replay", modules: { - "ws:github": { + "codemode:github": { postComment: async ([issue, body], ctx) => { const { approved } = await ctx.interrupt<{ approved: boolean }>({ issue, @@ -684,9 +684,10 @@ Pass these to the `modules` option of `WorkerJavaScriptBackend`: | Function | Entry point | Module | Description | | ------------------------- | ---------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------ | -| `createGitModule()` | `@cloudflare/computer/modules/git` | `ws:git` | Git operations on the workspace. Requires the `git` workspace option. Pass `{ allowNetwork: true }` to clone, fetch, and push. | -| `createArtifactsModule()` | `@cloudflare/computer/modules/artifacts` | `ws:artifacts` | [Artifacts](/artifacts/) repositories. Requires the `artifacts` workspace option. | -| `createToolBindings()` | `@cloudflare/computer/modules/tools` | `ws:tools` | The agent's own tools, callable from code. Build the bindings with `forAISDKTools()` or `forPiTools()`. | +| `createGitModule()` | `@cloudflare/computer/modules/git` | `codemode:git` | Git operations on the workspace. Requires the `git` workspace option. Pass `{ allowNetwork: true }` to clone, fetch, and push. | +| `createArtifactsModule()` | `@cloudflare/computer/modules/artifacts` | `codemode:artifacts` | [Artifacts](/artifacts/) repositories. Requires the `artifacts` workspace option. | +| `createToolBindings()` | `@cloudflare/computer/modules/tools` | `codemode:tools` | The agent's own tools, callable from code. Build the bindings with `forAISDKTools()` or `forPiTools()`. | +| `createContainerModule()` | `@cloudflare/computer/modules/container` | `codemode:container` | Shell commands in the workspace's container backend. | For the full interfaces, refer to the [Computer design documentation](https://github.com/cloudflare/computer/tree/main/docs). diff --git a/src/content/docs/agents/tools/index.mdx b/src/content/docs/agents/tools/index.mdx index 56e6419587c..7c94976e8a3 100644 --- a/src/content/docs/agents/tools/index.mdx +++ b/src/content/docs/agents/tools/index.mdx @@ -9,6 +9,6 @@ sidebar: import { DirectoryListing } from "~/components"; -Tools give agents capabilities such as [searching the web](/agents/tools/web-search/), [reading webpages and documents](/agents/tools/web-fetch/), browsing the web, processing payments, and running code. Agents can call tools in different ways. Use [Code Mode](/agents/tools/codemode/) to let models discover and orchestrate multiple tools by writing code, or use direct tool calls for simple actions. +Tools give agents capabilities such as [searching the web](/agents/tools/web-search/), [reading webpages and documents](/agents/tools/web-fetch/), browsing the web, processing payments, and running code. The model can call each tool directly, or write code that calls several of them in one run. For the second, refer to [Code Mode](/agents/concepts/code-mode/). diff --git a/src/content/docs/agents/tools/mcp.mdx b/src/content/docs/agents/tools/mcp.mdx index 6c718620c09..f855005f11f 100644 --- a/src/content/docs/agents/tools/mcp.mdx +++ b/src/content/docs/agents/tools/mcp.mdx @@ -92,9 +92,9 @@ await this.addMcpServer("internal", this.env.MCP_SERVER_URL, { /> + This tutorial uses the AI Search and Worker Loader bindings, which require Wrangler v4. If `create-cloudflare` set up your project with an earlier version, upgrade it: @@ -73,7 +78,7 @@ This tutorial uses the AI Search and Worker Loader bindings, which require Wrang ## 2. Configure Wrangler -Replace your [Wrangler configuration file](/workers/wrangler/configuration/) with the following. This adds the AI Search binding, a Workers AI binding for the model, a Worker Loader binding that runs the model's code in an isolated Worker, and the Durable Object that stores the agent's chat history and durable runtime state. +Replace your [Wrangler configuration file](/workers/wrangler/configuration/) with the following. It adds the AI Search binding, a Workers AI binding for the model, a Worker Loader binding that runs the model's code in a Dynamic Worker, and the Durable Object that holds the agent. @@ -96,127 +101,33 @@ remote = true binding = "LOADER" [[durable_objects.bindings]] -name = "Chat" -class_name = "Chat" +name = "KnowledgeAgent" +class_name = "KnowledgeAgent" [[migrations]] tag = "v1" -new_sqlite_classes = ["Chat"] +new_sqlite_classes = ["KnowledgeAgent"] ``` -AI Search has no local emulator, so the binding always talks to the remote service (`remote = true`). Because of this, you exercise the agent by deploying it rather than with `wrangler dev`. `AIChatAgent` persists messages to SQLite, so its class must be listed in `new_sqlite_classes`. - -## 3. Create the AI Search connector - -Create `src/ai-search-connector.ts`. The connector calls the AI Search binding directly, so requests stay in-process and no public endpoint is required. - -Give the model a read-only `search` method and a `saveDocument` method. Because `saveDocument` writes content, mark it `requiresApproval` and add a `revert` so the runtime can roll it back. - - - -```ts -import { CodemodeConnector, type ConnectorTools } from "@cloudflare/codemode"; - -// The instance this connector reads from and writes to. -const INSTANCE_NAME = "knowledge-base"; - -// A connector turns AI Search operations into methods the model can call from -// its generated code. Each connector becomes one named object in the sandbox. -export class AISearchConnector extends CodemodeConnector { - // The sandbox global. The model calls `aiSearch.search()` and - // `aiSearch.saveDocument()`. - override name() { - return "aiSearch"; - } - - // Shown to the model so it knows what this connector is for. - protected override instructions() { - return "Use this connector to search indexed content and save new documents."; - } - - // Every method the model can call. `inputSchema` is JSON Schema; the runtime - // validates the model's arguments against it before calling `execute`. - protected override tools(): ConnectorTools { - return { - // Read-only, so it runs without approval. - search: { - description: "Search indexed content and return the matching chunks.", - inputSchema: { - type: "object", - properties: { query: { type: "string" } }, - required: ["query"], - }, - execute: async (input) => { - const { query } = input as { query: string }; - return this.env.AI_SEARCH.get(INSTANCE_NAME).search({ - query, - ai_search_options: { retrieval: { max_num_results: 5 } }, - }); - }, - }, - // Writes content, so it is gated behind approval and made reversible. - saveDocument: { - description: "Save a new document to the knowledge base.", - inputSchema: { - type: "object", - properties: { - name: { type: "string" }, - content: { type: "string" }, - }, - required: ["name", "content"], - }, - // Pauses the run before this method executes so a human can approve it. - requiresApproval: true, - execute: async (input) => { - const { name, content } = input as { name: string; content: string }; - // upload() queues the document for indexing and returns right away. - // The item becomes searchable once indexing finishes, a few seconds later. - const item = await this.env.AI_SEARCH.get(INSTANCE_NAME).items.upload( - name, - content, - ); - // This return value is passed to `revert` if the call is rolled back. - return { id: item.id, key: item.key, status: item.status }; - }, - // Compensating action for rollback: delete the document this call added. - revert: async (_input, result) => { - const { id } = result as { id: string }; - await this.env.AI_SEARCH.get(INSTANCE_NAME).items.delete(id); - }, - }, - }; - } -} -``` - - +AI Search has no local emulator, so the binding always talks to the remote service (`remote = true`). Because of this, you try the agent by deploying it rather than with `wrangler dev`. The agent stores its workspace, paused runs, and saved documents in SQLite, so its class must be listed in `new_sqlite_classes`. -The `name()` result (`aiSearch`) becomes the global the model's code calls, so the methods are available as `aiSearch.search()` and `aiSearch.saveDocument()`. +## 3. Build the agent -## 4. Build the agent - -Create `src/server.ts`. New AI Search instances use [hybrid search](/ai-search/configuration/indexing/hybrid-search/) by default. The example then creates the Code Mode runtime with the connector and exposes it to the model as a single `codemode` tool. The `@callable()` methods let your client list pending approvals and approve, reject, or roll back a write. +Create `src/server.ts`: ```ts -import { AIChatAgent } from "@cloudflare/ai-chat"; -import { - createCodemodeRuntime, - DynamicWorkerExecutor, - type CodemodeRuntimeHandle, - type PendingAction, -} from "@cloudflare/codemode"; -import { callable, routeAgentRequest } from "agents"; -import { createWorkersAI } from "workers-ai-provider"; -import { convertToModelMessages, stepCountIs, streamText } from "ai"; -import { AISearchConnector } from "./ai-search-connector"; - -// Code Mode stores its durable state (execution log, pending approvals) in a -// facet exported from the Worker entry. The runtime requires this export. -export { CodemodeRuntime } from "@cloudflare/codemode"; +import { DurableObject } from "cloudflare:workers"; +import { Workspace, type WorkspaceModuleFunctions } from "@cloudflare/computer"; +import { WorkerJavaScriptBackend } from "@cloudflare/computer/backends/worker-javascript"; +import { createAITools } from "@cloudflare/computer/tools/ai-sdk"; +import { Lifecycle } from "agents/lifecycle"; +import { createAI } from "agents/models/ai-sdk"; +import { routeAgentRequest } from "agents/routing"; +import { generateText, stepCountIs } from "ai"; const INSTANCE_NAME = "knowledge-base"; @@ -224,106 +135,150 @@ const INSTANCE_NAME = "knowledge-base"; const SEED_DOC = `# Getting started AI Search indexes your content so an agent can search it and add to it.`; -export class Chat extends AIChatAgent { - // In-memory guard, so the one-time setup runs once per instance lifetime. - private ready = false; - - // Create the AI Search instance, which uses hybrid search by default, then seed it. - private async ensureInstance() { - if (this.ready) return; +export class KnowledgeAgent extends DurableObject { + readonly lifecycle = Lifecycle.install(this); + + workspace = new Workspace({ + storage: this.ctx.storage, + backends: [ + new WorkerJavaScriptBackend({ + loader: this.env.LOADER, + // Record each run in a journal, so it can pause for approval and resume. + onInterrupt: "replay", + modules: { + "codemode:kb": { + search: ([query]) => this.search(String(query)), + saveDocument: async ([doc], ctx) => { + // Arguments come from model-written code, so check them first. + const { name, content } = (doc ?? {}) as { name?: unknown; content?: unknown }; + if (typeof name !== "string" || typeof content !== "string") { + throw new Error("saveDocument takes { name: string, content: string }."); + } + // Pause the run until a person answers. + const { approved } = await ctx.interrupt<{ approved: boolean }>({ name, content }); + if (!approved) throw new Error("A person rejected this save. Do not retry it."); + return this.saveDocument(name, content, ctx.callId); + }, + } satisfies WorkspaceModuleFunctions, + }, + }), + ], + }); + + // Runs once before the first request: create the table and the AI Search instance. + async onStart() { + this.ctx.storage.sql.exec( + `CREATE TABLE IF NOT EXISTS saved_documents ( + call_id TEXT PRIMARY KEY, item_id TEXT NOT NULL, name TEXT NOT NULL + )`, + ); try { - await this.env.AI_SEARCH.create({ - id: INSTANCE_NAME, - }); - // Queue the seed document for indexing so the first search has content. - await this.env.AI_SEARCH.get(INSTANCE_NAME).items.upload( - "getting-started.md", - SEED_DOC, - ); + await this.env.AI_SEARCH.create({ id: INSTANCE_NAME }); + await this.env.AI_SEARCH.get(INSTANCE_NAME).items.upload("getting-started.md", SEED_DOC); } catch { - // intentional fallback, nothing to handle + // The instance already exists. } - this.ready = true; } - // Build the Code Mode runtime for this request. The handle is cheap to - // create; the durable state lives in the Durable Object, not the handle. - #runtime(): CodemodeRuntimeHandle { - return createCodemodeRuntime({ - ctx: this.ctx, - // Runs the model's generated code in an isolated Worker. - executor: new DynamicWorkerExecutor({ loader: this.env.LOADER }), - connectors: [new AISearchConnector(this.ctx, this.env)], + async search(query: string): Promise { + const results = await this.env.AI_SEARCH.get(INSTANCE_NAME).search({ + query, + ai_search_options: { retrieval: { max_num_results: 5 } }, }); + // Host module results must be plain JSON data. + return JSON.parse(JSON.stringify(results)); } - // Runs on every chat message from the client. - async onChatMessage() { - await this.ensureInstance(); - - const workersai = createWorkersAI({ binding: this.env.AI }); - - const result = streamText({ - model: workersai("@cf/zai-org/glm-5.2"), - system: - "You help maintain a knowledge base. Use codemode to search existing " + - "content and to save new documents. Search before you answer.", - messages: await convertToModelMessages(this.messages), - // The model sees one `codemode` tool and writes code that calls the connector. - tools: { codemode: this.#runtime().tool() }, - // Cap the agent's tool-use loop. - stopWhen: stepCountIs(10), - }); - - return result.toUIMessageStreamResponse(); + // Called by `saveDocument()` in the model's code, after a person approves it. + async saveDocument(name: string, content: string, callId: string) { + // A replay after a restart can call this again with the same callId. + const existing = this.ctx.storage.sql + .exec<{ item_id: string }>("SELECT item_id FROM saved_documents WHERE call_id = ?", callId) + .toArray()[0]; + if (existing) return { id: existing.item_id, name }; + + const item = await this.env.AI_SEARCH.get(INSTANCE_NAME).items.upload(name, content); + this.ctx.storage.sql.exec( + "INSERT INTO saved_documents (call_id, item_id, name) VALUES (?, ?, ?)", + callId, + item.id, + name, + ); + return { id: item.id, name, status: item.status }; } - // The methods below are called from your client to drive the approval flow. + // HTTP API for the chat and the approval flow. + async onRequest(request: Request): Promise { + const url = new URL(request.url); + const action = url.pathname.split("/").pop(); + + if (action === "ask" && request.method === "POST") { + const { prompt } = await request.json<{ prompt: string }>(); + const { text } = await generateText({ + model: createAI({ binding: this.env.AI })("@cf/moonshotai/kimi-k2.6"), + system: + "You help maintain a knowledge base. Use exec to run JavaScript that imports " + + "from codemode:kb: search(query) returns matching chunks, and " + + "saveDocument({ name, content }) saves a new document after a person approves it. " + + "Search before you save. If a save is rejected, stop and tell the user.", + prompt, + tools: createAITools({ workspace: this.workspace, exec: { "worker-javascript": {} } }), + stopWhen: stepCountIs(10), + }); + return Response.json({ text }); + } - // List the writes that are paused waiting for approval. - @callable() - async pendingApprovals(): Promise { - return this.#runtime().pending(); - } + if (action === "pending") { + return Response.json(await this.workspace.runtime.pending()); + } - // Approve a paused write. The runtime resumes the program and runs it. - @callable() - async approveExecution(executionId: string) { - return this.#runtime().approve({ executionId }); - } + if (action === "respond" && request.method === "POST") { + const { id, callId, approved } = await request.json<{ id: string; callId: string; approved: boolean }>(); + await this.workspace.runtime.respond(id, callId, { approved }); + return Response.json({ ok: true }); + } - // Decline a paused write. The execution ends without saving. - @callable() - async rejectExecution(executionId: string, seq: number): Promise { - return this.#runtime().reject({ executionId, seq }); - } + if (action === "saved") { + return Response.json(this.ctx.storage.sql.exec("SELECT * FROM saved_documents").toArray()); + } + + if (action === "rollback" && request.method === "POST") { + const { callId } = await request.json<{ callId: string }>(); + const row = this.ctx.storage.sql + .exec<{ item_id: string }>("SELECT item_id FROM saved_documents WHERE call_id = ?", callId) + .toArray()[0]; + if (!row) return Response.json({ error: "No saved document for that call." }, { status: 404 }); + await this.env.AI_SEARCH.get(INSTANCE_NAME).items.delete(row.item_id); + this.ctx.storage.sql.exec("DELETE FROM saved_documents WHERE call_id = ?", callId); + return Response.json({ deleted: row.item_id }); + } - // Undo an applied write by running the connector's revert. - @callable() - async rollbackExecution(executionId: string): Promise { - await this.#runtime().rollback({ executionId }); + return new Response("Not found", { status: 404 }); } } export default { async fetch(request: Request, env: Env) { - return ( - (await routeAgentRequest(request, env)) || - new Response("Not found", { status: 404 }) - ); + return (await routeAgentRequest(request, env)) ?? new Response("Not found", { status: 404 }); }, } satisfies ExportedHandler; ``` +The agent has three parts: + +- **The workspace.** A Computer `Workspace` with one `WorkerJavaScriptBackend`. `onInterrupt: "replay"` journals each run, which lets a run pause and resume. `satisfies WorkspaceModuleFunctions` gives the module's functions their argument types. +- **The `codemode:kb` module.** `search` and `saveDocument` call AI Search with the agent's binding. The model's code never sees the binding. `saveDocument` checks its argument before it pauses, because the argument comes from model-written code. It passes `ctx.callId` on, so a replay after a restart does not upload the same document twice. +- **The HTTP API.** `/ask` runs one model turn. `createAITools()` gives the model the `exec` tool. `/pending` and `/respond` drive the approval, and `/saved` and `/rollback` undo a save. + Generate types: ```sh npx wrangler types ``` -## 5. Deploy +## 4. Deploy Because AI Search runs remotely, you deploy the Worker to run the agent. @@ -341,113 +296,101 @@ npx wrangler deploy Wrangler prints your Worker's URL, for example `https://kb-agent..workers.dev`. You use it in the next step. -## 6. Try the approval and rollback flow +## 5. Try the approval and rollback flow -The model receives one `codemode` tool. When you ask it to find and save content, it writes a short program that calls the connector methods: +When you ask the agent to find and save content, the model writes a program like this one and runs it with `exec`: ```js -// The model writes this program. It runs inside the Code Mode sandbox. -async () => { - // The read-only search runs immediately. - const existing = await aiSearch.search({ query: "onboarding steps" }); +import { search, saveDocument } from "codemode:kb"; + +export default async function () { + // The read-only search runs straight away. + const existing = await search("onboarding steps"); // Only save when the knowledge base has no matching content yet. if (existing.chunks.length === 0) { - // saveDocument requires approval, so this call pauses the program here. - await aiSearch.saveDocument({ + // The program pauses here until a person answers. + return saveDocument({ name: "onboarding.md", content: "# Onboarding\nStep 1: create an account.", }); } - return existing.chunks.length; -}; +} ``` -`aiSearch.search()` runs immediately. When the program reaches `aiSearch.saveDocument()`, the runtime records the call as pending and pauses the execution before the upload runs. +`search()` runs straight away. When the program reaches `saveDocument()`, the run pauses before the upload. The `/ask` request stays open while the run waits. -Your client sends the chat message that starts the run, then drives the approval with the `@callable()` methods. The following script uses the [Agents SDK client](/agents/agent-class/communication-channels/chat/client-sdk/) to do both. Save it as `client.mjs`, set `HOST` to your deployed Worker, and run it with `node client.mjs`: +The following script sends a question, approves the save, and prints the answer. Save it as `client.mjs`, set `BASE` to your deployed Worker, and run it with `node client.mjs`: ```js -import { AgentClient } from "agents/client"; - -// Your deployed Worker, without the protocol. -const HOST = "kb-agent..workers.dev"; - -const client = new AgentClient({ agent: "Chat", name: "default", host: HOST }); -await client.ready; - -// 1. Ask the agent to find and save content. It writes a Code Mode program; -// the saveDocument call pauses for approval instead of running. -client.send( - JSON.stringify({ - type: "cf_agent_use_chat_request", - id: crypto.randomUUID(), - init: { - method: "POST", - body: JSON.stringify({ - messages: [ - { - id: crypto.randomUUID(), - role: "user", - parts: [ - { - type: "text", - text: "Search for onboarding steps. If there is none, save a document named onboarding.md with a short onboarding guide.", - }, - ], - }, - ], - }), - }, +// Your deployed Worker, and the name of the agent instance. +const BASE = "https://kb-agent..workers.dev/agents/knowledge-agent/default"; + +// 1. Ask the agent to find and save content. Do not wait for the answer yet: +// the request stays open while saveDocument waits for approval. +const answer = fetch(`${BASE}/ask`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + prompt: + "Search for onboarding steps. If there are none, save a document named onboarding.md with a short onboarding guide.", }), -); - -// Give the turn time to run and pause at saveDocument. -await new Promise((r) => setTimeout(r, 20_000)); +}).then((response) => response.json()); -// 2. List the writes waiting for approval. -const pending = await client.call("pendingApprovals"); -console.log("Pending approvals:", pending); - -// 3. Approve the first one. The runtime replays the program: completed calls -// return their recorded results, and the approved saveDocument runs. -if (pending.length > 0) { - await client.call("approveExecution", [pending[0].executionId]); - console.log("Approved", pending[0].executionId); - - // To roll back later, delete the uploaded document: - // await client.call("rollbackExecution", [pending[0].executionId]); +// 2. Wait for the save to pause, then show it. +let pending = []; +while (pending.length === 0) { + await new Promise((resolve) => setTimeout(resolve, 2000)); + pending = await (await fetch(`${BASE}/pending`)).json(); } - -client.close(); +const [call] = pending; +console.log("Waiting for approval:", call.payload.name); + +// 3. Approve it. Send approved: false to reject it instead. +await fetch(`${BASE}/respond`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ id: call.id, callId: call.callId, approved: true }), +}); + +// 4. The program resumes, the document is saved, and the model answers. +console.log((await answer).text); + +// To roll back the save later, delete the uploaded document: +// await fetch(`${BASE}/rollback`, { +// method: "POST", +// headers: { "content-type": "application/json" }, +// body: JSON.stringify({ callId: call.callId }), +// }); ``` -Each `PendingAction` from `pendingApprovals()` includes the `executionId`, a `seq` number, and the method and arguments, so you can show the pending document to the user before deciding. The approval methods behave as follows: +Each call from `/pending` has the execution `id`, the `callId`, and the `payload` that `saveDocument` passed to `ctx.interrupt()`, so you can show the document to a person before they decide. The endpoints behave as follows: -- `approveExecution(executionId)` replays the program and runs the approved `saveDocument`. The document is queued for indexing and becomes searchable a few seconds later. -- `rejectExecution(executionId, seq)` ends the execution without saving. -- `rollbackExecution(executionId)` undoes an applied write by running the connector's `revert`, which deletes the uploaded document. +- `POST /respond` with `approved: true` resumes the program, and the document is uploaded. AI Search indexes it and makes it searchable a few seconds later. +- `POST /respond` with `approved: false` makes `saveDocument` throw. The model sees the error and tells the user the save was rejected. +- `GET /saved` lists the saved documents, with the `callId` of each save. +- `POST /rollback` with a `callId` deletes the document that save uploaded. ## What you built Your agent can now: -- Search the knowledge base with a read-only tool. -- Propose new documents through a write tool that pauses for human approval. -- Resume the same program after approval, without re-running completed work. +- Search the knowledge base from model-written code. +- Propose new documents through a function that pauses for approval. +- Resume the same program after approval, without running completed calls again. - Roll back an approved save by deleting the indexed document. ## Next steps