← Создание агентов на Cloudflare / agents / tools / codemode
Создайте устойчивый рантайм Code Mode
Это руководство добавляет устойчивый рантайм Code Mode в приложение на базе Agents SDK. Рантайм сохраняет историю выполнения, незавершённые подтверждения и сниппеты даже при гибернации Durable Object.
Предварительные требования
Вам понадобится существующее приложение на Agents SDK с Durable Object и Vite. В примере используется AIChatAgent и AI SDK.
Интеграция Code Mode
-
Установите пакет Code Mode:
npm i @cloudflare/codemode -
Добавьте привязку Worker Loader.
DynamicWorkerExecutorиспользует эту привязку для выполнения сгенерированного моделью кода в изолированных Workers:{ "$schema": "./node_modules/wrangler/config-schema.json", // Set this to today's date "compatibility_date": "2026-08-28", "compatibility_flags": [ "nodejs_compat" ], "worker_loaders": [ { "binding": "LOADER" } ] }# Set this to today's date compatibility_date = "2026-08-28" compatibility_flags = ["nodejs_compat"] [[worker_loaders]] binding = "LOADER" -
Добавьте плагины Agents и Code Mode в
vite.config.ts:vite.config.jsimport { 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.tsimport { 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()], });Плагин экспортирует
CodemodeRuntimeкласс грани из точки входа вашего Worker. Runtime хранит состояние выполнения в грани Durable Object, а Workers runtime требует, чтобы классы граней были доступны черезctx.exports. Если вы не используете плагин, добавьте экспорт вручную:export { CodemodeRuntime } from "@cloudflare/codemode"; -
Создайте коннектор. Коннекторы представляют собой простые классы, для которых не нужны специальные имена файлов или синтаксис импорта. В этом примере заметки сохраняются в хранилище Durable Object объекта Agent:
src/notes-connector.jsimport { 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.tsimport { 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, в данном случаеnotes.requiresApproval: trueприостанавливается передcreateNoteвыполняется. Необязательныйrevertфункция позволяетruntime.rollback()компенсирует применённый вызов.Используйте
McpConnectorдля инструментов MCP илиOpenApiConnectorдля операций OpenAPI. Настройку, специфичную для MCP, см. в Используйте инструменты MCP с Code Mode. -
Импортируйте коннектор и создайте рантайм в своём Agent:
src/server.jsimport { 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.tsimport { 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импорт с существующей настройкой модели в вашем приложении.
Используйте runtime без AI SDK
Используйте execute(), search(), а также describe() когда MCP-сервер или другой хост вызывает Code Mode без адаптера инструментов AI SDK:
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() возвращает тот же durable result, что и инструмент, обращённый к модели. Result может завершиться, приостановиться или содержать ошибку выполнения. Разрешите приостановленный result с помощью approve() или reject().
Проверка интеграции
Попросите модель вывести список сохранённых заметок. Модель получает один codemode инструмент и может обнаруживать методы коннектора внутри sandbox:
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() чтобы показать ожидающее действие. Передайте его executionId к approveExecution(), либо передайте оба executionId и seq к rejectExecution().
Подтверждение возобновляет тот же сценарий через воспроизведение. Завершённые вызовы возвращают записанные результаты вместо повторного выполнения. Отклонение завершает приостановленное выполнение, не отменяя более ранние действия.
Вызовите rollbackExecution() чтобы компенсировать применённые вызовы, для которых текущий настроенный коннектор предоставляет revert. Сохраняйте как сниппеты только завершённые выполнения.