INTEGRITY Документация

Как работает Code Mode

Code Mode: паттерн, при котором модель пишет код для комбинирования инструментов. @cloudflare/codemode пакет реализует этот паттерн с изолированным executor, коннекторами сервисов и устойчивым runtime.

У этих частей разные зоны ответственности. Executor выполняет код, но не хранит состояние. Коннекторы предоставляют возможности, но не управляют повторным воспроизведением. Рантайм фиксирует выполнение и управляет подтверждениями, повторным воспроизведением, откатом и повторным использованием.

Что видит модель

В стандартной настройке модель получает один внешний инструмент с именем codemode. Этот инструмент принимает одно поле и возвращает устойчивый результат выполнения:

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

В описании модели предлагается написать асинхронную стрелочную функцию JavaScript. Там же перечислены имена настроенных пространств имён коннекторов, например github или stripe, но он включает не все методы и схемы коннектора.

Модель может использовать одно выполнение Code Mode, чтобы обнаружить нужные методы, а затем использовать возвращённые пути и типы в следующем выполнении. Это позволяет не включать полный каталог инструментов в исходный контекст модели.

Platform SDK

Внутри песочницы codemode глобальная переменная предоставляет SDK уровня платформы:

declare const codemode: {
	search(query: string): Promise<SearchOutput>;
	describe(target: string): Promise<DescribeOutput>;
	step<T>(name: string, fn: () => T | Promise<T>): Promise<T>;
	run(name: string, input?: unknown): Promise<unknown>;
};

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() выполняет поиск по методам коннектора и сохранённым сниппетам. Он возвращает ранжированные пути, а не полные схемы. После этого модель может передать один путь в codemode.describe() чтобы запросить целевую документацию TypeScript.

codemode.step() записывает недетерминированную работу в sandbox с побочными эффектами для последующего воспроизведения. codemode.run() вызывает сохранённый фрагмент кода.

SDK коннекторов

Каждый настроенный коннектор становится ещё одной глобальной переменной песочницы. Коннектор с именем github доступен как github, и его методы отображаются по путям вида github.list_pull_requests.

Описание уровня коннектора возвращает объявления, похожие на:

type ListPullRequestsInput = {
	owner: string;
	repo: string;
	state?: "open" | "closed";
};

type ListPullRequestsOutput = unknown;

declare const github: {
	list_pull_requests(
		input: ListPullRequestsInput,
	): Promise<ListPullRequestsOutput>;
};

Эти декларации генерируются на основе схем коннекторов. Они носят иллюстративный характер: фактические имена методов, входные поля и типы вывода зависят от конкретного коннектора.

Sandbox также включает стандартные глобальные объекты JavaScript. Он не предоставляет доступ к Node.js API, учётным данным хоста, process, require, или неограниченный доступ к сети. Все внешние операции проходят через глобальные объекты коннектора, если только executor явно не предоставляет другую возможность.

Executor, коннекторы и среда выполнения

Executor

Executor один раз выполняет один блок сгенерированного моделью кода. Он получает вызываемые пространства имён и возвращает результат, ошибку и перехваченный вывод консоли. Он не сохраняет историю выполнения.

DynamicWorkerExecutor использует Dynamic Worker Loader чтобы создавать изолированный Worker для каждого прохода выполнения. Возобновлённое выполнение снова запускает код в другом проходе. Поэтому устойчивое состояние не может храниться внутри песочницы.

Внешние fetch() и connect() вызовы по умолчанию блокируются. DynamicWorkerExecutor настраивает globalOutbound: null если вы не укажете другое значение. Вы можете указать Fetcher чтобы направлять исходящие запросы через контролируемый сервис.

Connectors

Коннекторы связывают сервисы на стороне хоста с sandbox. Коннектор может оборачивать MCP-сервер (Model Context Protocol), документ OpenAPI, набор инструментов AI SDK или произвольный код.

Каждый коннектор становится отдельным пространством имён. Например, коннектор с именем github предоставляет вызовы, такие как github.list_pull_requests(). Сгенерированный код никогда не получает учётные данные коннектора или объекты клиента.

Вызовы коннекторов пересекают границу sandbox через Удалённые вызовы процедур (RPC) в Workers. Рантайм перехватывает каждый вызов до того, как его выполнит коннектор. Это перехватывание применяет подтверждение, логирование, повтор и политику отката.

codemode глобальная переменная предоставляет операции обнаружения и операции времени выполнения. codemode.search() находит методы коннекторов и сохранённые сниппеты. codemode.describe() возвращает целевую документацию TypeScript, не помещая в контекст модели все схемы коннекторов.

Устойчивый рантайм

Runtime связывает executor и коннекторы. Он хранит записи выполнения, логи вызовов коннекторов, ожидающие подтверждения и сниппеты в изолированном хранилище SQLite. Это состояние сохраняется после завершения запроса и гибернации Durable Object.

Экземпляры executor и коннектора остаются временными. Ваше приложение предоставляет их заново при обработке последующего подтверждения или запроса.

Типичный Agent создаёт все три части вместе:

src/server.js
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() };
src/server.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 хранит это состояние в грани Durable Object. Грань: это долговечный дочерний объект Agent с собственным хранилищем SQLite. createCodemodeRuntime() и плагин Vite сами управляют этой деталью реализации. Вы не создаёте грань и не обращаетесь к ней напрямую.

Используйте несколько runtime

Большинству Agents нужен только один рантайм Code Mode. Если вы не укажете name, среда выполнения использует имя default.

Задайте name когда одному Agent нужны отдельные истории Code Mode. Например, runtime с именем research и ещё один с именем operations хранят отдельные записи о выполнении и коллекции сниппетов:

src/server.js
const researchRuntime = createCodemodeRuntime({
	ctx: this.ctx,
	executor,
	connectors: researchConnectors,
	name: "research",
});

const operationsRuntime = createCodemodeRuntime({
	ctx: this.ctx,
	executor,
	connectors: operationsConnectors,
	name: "operations",
});
src/server.ts
const researchRuntime = createCodemodeRuntime({
	ctx: this.ctx,
	executor,
	connectors: researchConnectors,
	name: "research",
});

const operationsRuntime = createCodemodeRuntime({
	ctx: this.ctx,
	executor,
	connectors: operationsConnectors,
	name: "operations",
});

Имя рантайма идентифицирует его устойчивое хранилище. Оно не именует модель, коннектор, инструмент или отдельное выполнение.

Изменение набора коннекторов не создаёт новый рантайм. Каждый запуск фиксирует все коннекторы, настроенные на момент его начала, и сохранённый сниппет наследует этот список. Повтор подтверждения и выполнение сниппета требуют, чтобы все зафиксированные коннекторы оставались доступны, даже если исходный код обращался не ко всем из них.

Лог устойчивого выполнения

Runtime присваивает каждому выполнению стабильный ID. Каждый вызов коннектора и codemode.step() запись получает порядковый номер. Журнал фиксирует её аргументы, состояние, политику воспроизведения и результат, если применимо.

Runtime помечает вызов как executing перед вызовом своего коннектора. Он помечает вызов как applied после записи результата. Если хост останавливается до завершения прохода, выполнение может остаться в running. Более поздний expirePaused() служебный вызов помечает это устаревшее выполнение как ошибку и освобождает его ресурсы. Одобрение не возобновляет устаревший running выполнение.

Этот журнал служит основой для повторного воспроизведения. Он также поддерживает представления для аудита разработчиков и определяет, какие действия можно откатить. Это не универсальная память бесед, и он не заменяет состояние Agent.

Подтверждения через прерывание и воспроизведение

Метод коннектора может требовать подтверждения пользователя. Когда сгенерированный код доходит до этого метода, рантайм помечает действие как ожидающее и прерывает текущий проход. Действие не получает предварительного результата.

Приложение может показать пользователю ожидающий метод и его аргументы. Подтверждение запускает новый проход с тем же исходным кодом и тем же ID выполнения. Вызовы, уже помеченные как применённые, возвращают сохранённые результаты вместо повторного выполнения. После этого выполняется подтверждённое действие, и код продолжает работу до завершения или следующего подтверждения.

first pass:   read ── execute ──> result
              write ── pause

approval

second pass:  read ── replay ───> recorded result
              write ── execute ─> result
              next call ────────> continue

Такая архитектура позволяет подтверждению переживать завершение запроса или гибернацию. Сгенерированный код остаётся линейным и не реализует логику паузы или возобновления.

Возобновить можно только приостановленное выполнение. Устаревшее подтверждение не может восстановить завершённое, отклонённое выполнение или выполнение после отката. Отклонение действия завершает выполнение, но не отменяет более ранние действия. Откат является отдельной операцией.

Сбои выполнения возвращаются в агентный цикл как данные. Поэтому ошибки sandbox и расхождения воспроизведения не должны выходить наружу в виде необработанных исключений RPC.

Детерминированное воспроизведение

Для воспроизведения требуется, чтобы вызовы коннектора и шаги происходили в одном и том же порядке. В каждом проходе конкретный порядковый номер должен использовать один и тот же коннектор, метод и аргументы. При несовпадении выполнение завершается ошибкой расхождения воспроизведения.

Записанные результаты коннектора обеспечивают стабильность обычных ветвлений, зависящих от данных. Однако значения из Date.now(), Math.random(), или другие недетерминированные источники способны изменить поток управления или аргументы действия.

Используйте codemode.step() чтобы зафиксировать такую работу один раз. Рантайм записывает результат closure и возвращает это значение при повторном воспроизведении после подтверждения:

async () => {
	const createdAt = await codemode.step("created-at", () => Date.now());

	return github.create_issue({
		owner: "cloudflare",
		repo: "agents",
		title: `Review created at ${createdAt}`,
	});
};

Вызовы коннекторов уже проходят через рантайм, поэтому обёртка step для них не нужна. Используйте step для недетерминированных операций или операций с побочными эффектами вне вызовов коннекторов. Если вы явно разрешили прямой доступ к сети, сюда входят и прямые сетевые операции, которые не должны повторяться при повторном воспроизведении подтверждений.

Выполняйте вызовы коннектора последовательно, если выполнение может приостанавливаться. Хост присваивает порядковые номера по мере поступления вызовов. Вызовы в Promise.all() могут приходить в разном порядке в разных проходах и вызывать расхождение при повторном воспроизведении.

Время жизни ресурсов

Некоторым коннекторам требуются ресурсы, выходящие за рамки одного вызова метода. Например, сессии браузера, транзакции базы данных и временные рабочие пространства. Методы коннектора получают устойчивый ID выполнения, который можно использовать как ключ для устойчивых метаданных ресурсов между проходами.

Code Mode различает два срока жизни ресурсов:

Приостановленное выполнение может возобновиться в другом вызове Worker. Хуки жизненного цикла коннектора не должны зависеть от памяти экземпляра. Очистка также должна быть идемпотентной, поскольку завершённое выполнение может быть позже откачено и снова освобождено.

Runtime вызывает lifecycle-хуки для каждого настроенного коннектора. Коннектор, который не выделял ресурс, должен безопасно ничего не делать. Ошибки очистки игнорируются, чтобы они не превращали завершённое выполнение в неудачное.

Политика воспроизведения

По умолчанию рантайм сохраняет результат коннектора и воспроизводит его при последующих проходах. Это сохраняет именно то значение, которое видел исходный код.

Коннектор может пометить вызов с помощью replay: "reexecute". Рантайм по-прежнему логирует последовательность вызовов и аргументы, но не сохраняет результат. Более поздний проход снова запускает метод коннектора.

Используйте эту политику только для идемпотентных операций чтения с большими, недорогими результатами. Результат может меняться между проходами, поэтому сгенерированный код должен учитывать это изменение. Методы, требующие подтверждения, не могут использовать replay: "reexecute" потому что повторное воспроизведение может применить одобренный побочный эффект более одного раза.

Откат

Откат проходит применённые вызовы коннектора в обратном порядке. Он вызывает revert реализацию для каждого применённого метода, который её предоставляет, независимо от того, требовал ли этот метод подтверждения.

Для каждого применённого вызова коннектора рантайм просит текущий настроенный коннектор выполнить его revert реализация. Методы без revert остаются применёнными. Отсутствующие коннекторы также пропускаются. Неудачный откат не останавливает последующие попытки компенсации, и рантайм сообщает об ошибках после того, как попробует оставшиеся вызовы. Выполнение переходит к rolled_back только если хотя бы один вызов был отменён.

Откат представляет собой компенсацию, а не изоляцию транзакций базы данных. Авторы коннекторов определяют, что означает отмена для каждого действия. Кроме того, внешняя система может измениться в промежутке между исходным вызовом и его компенсацией.

Хранение и устаревшие выполнения

Журнал выполнения представляет собой аудиторский след и со временем растёт. При начале нового запуска runtime сначала добавляет запись об этом запуске, а затем удаляет более старые завершённые выполнения. maxExecutions по умолчанию равно 50. Поскольку активное выполнение ещё не завершено, по завершении может временно остаться 51 конечная запись, пока не начнётся новый запуск или пока вы не вызовете pruneExecutions().

Выполняющиеся и приостановленные запуски не удаляются автоматически. Им может всё ещё требоваться завершение или возобновление. Используйте expirePaused() из регулярного обслуживания для освобождения устаревших незавершённых запусков. Среда выполнения помечает устаревшие приостановленные запуски как отклонённые, а устаревшие выполняющиеся запуски как ошибочные, а затем освобождает их ресурсы выполнения.

Вы также можете удалять отдельные записи о выполнении или явно очищать терминальную историю. Удаление нетерминального выполнения освобождает ресурсы, ограниченные областью этого выполнения.

Лимиты устойчивого значения и результата

Каждое значение, сохраняемое для устойчивого воспроизведения, имеет ограничение сериализованной длины в 1,000,000 символов. Реализация проверяет длину строки JavaScript после сериализации. Это ограничение применяется к аргументам коннектора, записанным результатам коннектора, результатам шагов и исходному коду выполнения.

Runtime не может усекать эти значения. Усечение привело бы к тому, что во время replay были бы доступны другие данные. Поэтому слишком большое или несериализуемое значение replay приводит к сбою выполнения и рекомендации хранить данные отдельно, передавая вместо них небольшую ссылку, например путь к файлу.

Финальный результат ведёт себя иначе, потому что replay его не потребляет. Выполнение может завершиться и вернуть модели настоящий результат. Если результат не помещается в запись аудита, рантайм сохраняет туда вместо этого сообщение о пропуске.

transformResult может преобразовать завершённый результат до того, как его получит модель. Преобразование выполняется уже после того, как runtime попытается записать исходный результат. В журнале аудита сохраняется исходное значение, если оно помещается, а модель при этом может получить уменьшённое представление.

Snippets

Snippet представляет собой сохранённый исходный код выполнения. Snippets превращают написанные моделью программы в переиспользуемые рецепты. Они остаются доступными между запросами и гибернацией.

Модель не продвигает собственный код самостоятельно. Ваше приложение проверяет выполнение и вызывает runtime.saveSnippet() с его ID выполнения. API принимает любой статус выполнения, поэтому перед сохранением убедитесь, что выполнение завершилось успешно. Затем модель сможет найти фрагмент с помощью codemode.search(), изучите его с помощью codemode.describe(), и вызовите его с codemode.run().

src/server.js
const runs = await runtime.executions(20);

await runtime.saveSnippet("list-open-prs", {
	executionId: runs[0].id,
	description: "List open pull requests for a repository.",
});
src/server.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.",
});

Snippet может принимать входное значение. Его вызовы коннекторов присоединяются к текущему журналу выполнения, когда модель его запускает. Snippet также сохраняет список коннекторов из исходного выполнения. Если записанный коннектор недоступен, codemode.run() разрешается в объект с error свойство. Само по себе оно не выбрасывает исключение.