Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

耐久的な Code Mode ランタイムを作成する

最終更新 Markdown で表示Agent セットアップ

このガイドでは、Agents SDK アプリケーションに耐久的な Code Mode ランタイムを追加します。ランタイムは、実行履歴、保留中の承認、スニペットを Durable Object のハイバネーションを越えて保存します。

前提条件

Durable Object と Vite を使う既存の Agents SDK アプリケーションが必要です。例では AIChatAgent と AI SDK を使います。

Code Mode を組み込む

  1. Code Mode パッケージをインストールします。

    npm i @cloudflare/codemode
  2. Worker Loader バインディングを追加します。DynamicWorkerExecutor はこのバインディングを使い、モデルが生成したコードを分離された Workers で実行します。

    {
      "$schema": "./node_modules/wrangler/config-schema.json",
      // Set this to today's date
      "compatibility_date": "2026-09-20",
      "compatibility_flags": [
        "nodejs_compat"
      ],
      "worker_loaders": [
        {
          "binding": "LOADER"
        }
      ]
    }
    # Set this to today's date
    compatibility_date = "2026-09-20"
    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. コネクタを作成します。コネクタは通常のクラスで、特別なファイル名や import 構文は不要です。この例では、メモを Agent の Durable Object ストレージに保存します。

    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() の結果がサンドボックスのグローバルになります。この場合は notes です。requiresApproval: true は、createNote の実行前に一時停止します。任意の revert 関数により、runtime.rollback() は適用済みの呼び出しを補償できます。

    MCP ツールには McpConnector、OpenAPI 操作には OpenApiConnector を使います。MCP 固有の設定は Code Mode で MCP ツールを使う を参照してください。

  5. コネクタを import し、Agent 内でランタイムを作成します。

    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 は、アプリケーション既存のモデル設定に置き換えてください。

AI SDK なしでランタイムを使う

MCP サーバーや別ホストが、AI SDK のツールアダプターなしで 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 ツールを 1 つ受け取り、サンドボックス内でコネクタメソッドを発見できます。

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

モデルが notes.createNote() を呼ぶと、実行は一時停止します。保留中のアクションは pendingApprovals() で表示します。承認するにはその executionIdapproveExecution() に渡し、却下するには executionIdseq の両方を rejectExecution() に渡します。

承認すると、同じスクリプトを replay(再実行)で再開します。完了済みの呼び出しは再実行せず、記録済みの結果を返します。却下すると、それまでのアクションは取り消さずに、一時停止中の実行を終了します。

現在設定されているコネクタが revert を提供する適用済み呼び出しを補償するには、rollbackExecution() を呼びます。スニペットとして保存できるのは、完了した実行だけです。

役に立ちましたか?