跳转到内容
搜索文档

创建持久 Code Mode 运行时

最后更新 查看 MarkdownAgent 设置

本指南为 Agents SDK 应用添加持久 Code Mode 运行时。该运行时在 Durable Object 休眠后仍存储执行历史、待审批项与代码片段。

前提条件

你需要一个现有 Agents SDK 应用,包含 Durable Object 与 Vite。示例使用 AIChatAgent 与 AI SDK。

集成 Code Mode

  1. 安装 Code Mode 包:

    npm i @cloudflare/codemode
  2. 添加 Worker Loader 绑定。DynamicWorkerExecutor 使用该绑定在隔离 Worker 中运行 model 生成的代码:

    {
      "$schema": "./node_modules/wrangler/config-schema.json",
      // Set this to today's date
      "compatibility_date": "2026-08-17",
      "compatibility_flags": [
        "nodejs_compat"
      ],
      "worker_loaders": [
        {
          "binding": "LOADER"
        }
      ]
    }
    # Set this to today's date
    compatibility_date = "2026-08-17"
    compatibility_flags = ["nodejs_compat"]
    
    [[worker_loaders]]
    binding = "LOADER"
  3. vite.config.ts 中添加 Agents 与 Code Mode 插件:

    vite.config.jsjs
    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()],
    });
    vite.config.tsts
    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()],
    });

    该插件从 Worker 入口模块导出 CodemodeRuntime facet 类。运行时将执行状态存储在 Durable Object facet 中,Workers 运行时要求 facet 类通过 ctx.exports 可用。若不使用插件,需手动添加导出:

    export { CodemodeRuntime } from "@cloudflare/codemode";
  4. 创建 connector。Connector 是普通类——无需特殊文件名或 import 语法。本示例将 note 存储在 Agent 的 Durable Object storage 中:

    src/notes-connector.jsjs
    import { CodemodeConnector } from "@cloudflare/codemode";
    
    export class NotesConnector extends CodemodeConnector {
    	storage;
    
    	constructor(ctx, env) {
    		super(ctx, env);
    		this.storage = ctx.storage;
    	}
    
    	name() {
    		return "notes";
    	}
    
    	instructions() {
    		return "Use this connector to list and create saved notes.";
    	}
    
    	tools() {
    		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;
    					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;
    					const notes = (await this.storage.get("notes")) ?? [];
    					await this.storage.put(
    						"notes",
    						notes.filter((note) => note.id !== id),
    					);
    				},
    			},
    		};
    	}
    }
    src/notes-connector.tsts
    import {
    	CodemodeConnector,
    	type ConnectorTools,
    } from "@cloudflare/codemode";
    
    type Note = { id: string; text: string };
    
    export class NotesConnector extends CodemodeConnector<Env> {
    	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<Note[]>("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<Note[]>("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<Note[]>("notes")) ?? [];
    					await this.storage.put(
    						"notes",
    						notes.filter((note) => note.id !== id),
    					);
    				},
    			},
    		};
    	}
    }

    name() 的返回值成为 sandbox 全局变量,本例为 notesrequiresApproval: true 会在 createNote 执行前暂停。可选的 revert 函数让 runtime.rollback() 补偿已应用的调用。

    对 MCP tool 使用 McpConnector,对 OpenAPI 操作使用 OpenApiConnector。MCP 特定设置请参阅在 Code Mode 中使用 MCP tool

  5. 导入 connector 并在 Agent 中创建 runtime:

    src/server.jsjs
    import { AIChatAgent } from "@cloudflare/ai-chat";
    import {
    	createCodemodeRuntime,
    	DynamicWorkerExecutor,
    } 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() {
    		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() {
    		return this.#runtime().pending();
    	}
    
    	@callable()
    	async approveExecution(executionId) {
    		return this.#runtime().approve({ executionId });
    	}
    
    	@callable()
    	async rejectExecution(executionId, seq) {
    		return this.#runtime().reject({ executionId, seq });
    	}
    
    	@callable()
    	async rollbackExecution(executionId) {
    		await this.#runtime().rollback({ executionId });
    	}
    
    	@callable()
    	async executionHistory() {
    		return this.#runtime().executions(20);
    	}
    
    	@callable()
    	async saveSnippet(name, description, executionId) {
    		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() {
    		return this.#runtime().snippets();
    	}
    }
    src/server.tsts
    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<Env> {
    	#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<PendingAction[]> {
    		return this.#runtime().pending();
    	}
    
    	@callable()
    	async approveExecution(executionId: string) {
    		return this.#runtime().approve({ executionId });
    	}
    
    	@callable()
    	async rejectExecution(executionId: string, seq: number): Promise<boolean> {
    		return this.#runtime().reject({ executionId, seq });
    	}
    
    	@callable()
    	async rollbackExecution(executionId: string): Promise<void> {
    		await this.#runtime().rollback({ executionId });
    	}
    
    	@callable()
    	async executionHistory(): Promise<ExecutionState[]> {
    		return this.#runtime().executions(20);
    	}
    
    	@callable()
    	async saveSnippet(
    		name: string,
    		description: string,
    		executionId: string,
    	): Promise<Snippet> {
    		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<Snippet[]> {
    		return this.#runtime().snippets();
    	}
    }

    model import 替换为应用中现有的 model 设置。

不使用 AI SDK 的运行时

当 MCP 服务器或其他 host 在不使用 AI SDK tool 适配器的情况下调用 Code Mode 时,使用 execute()search()describe()

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" })`,
});
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()describe() 不运行沙箱代码。其结果对执行前暂停的连接器方法包含 requiresApproval: true

execute() 返回与面向模型的工具相同的持久结果。结果可能完成、暂停或包含执行错误。用 approve()reject() 解决暂停结果。

验证集成

让模型列出已保存笔记。模型收到一个 codemode 工具,可在沙箱内发现连接器方法:

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 };
};

当 model 调用 notes.createNote() 时,执行暂停。用 pendingApprovals() 显示待处理操作。将其 executionId 传给 approveExecution(),或将 executionIdseq 传给 rejectExecution()

审批通过 replay 恢复同一脚本。已完成的调用返回记录结果而非再次运行。拒绝会结束暂停的执行,不会撤销更早的操作。

调用 rollbackExecution() 以补偿当前配置的 connector 提供 revert 的已应用调用。仅将已完成的执行保存为 snippet。

这篇文档对您有帮助吗?