branch:
execute.ts
5288 bytesRaw
import type { ToolSet } from "ai";
import { createCodeTool } from "@cloudflare/codemode/ai";
import { DynamicWorkerExecutor } from "@cloudflare/codemode";
import type { Executor, ToolProvider } from "@cloudflare/codemode";
import type { StateBackend } from "@cloudflare/shell";
import { stateToolsFromBackend } from "@cloudflare/shell/workers";

export interface CreateExecuteToolOptions {
  /**
   * The tools available inside the sandboxed code as `codemode.*`.
   *
   * Typically the workspace tools from `createWorkspaceTools()`,
   * but can include any AI SDK tools with `execute` functions.
   */
  tools: ToolSet;

  /**
   * Optional StateBackend to expose as `state.*` inside the sandbox.
   *
   * When provided, the sandbox has both `codemode.*` tool calls and
   * the full `state.*` filesystem API (readFile, writeFile, glob,
   * searchFiles, replaceInFiles, planEdits, etc.).
   *
   * This is the preferred way to give the LLM rich filesystem access:
   * use individual workspace tools for simple one-shot operations,
   * and `state.*` for coordinated multi-file work.
   *
   * @example
   * ```ts
   * import { createWorkspaceStateBackend } from "@cloudflare/shell";
   *
   * createExecuteTool({
   *   tools: myDomainTools,
   *   state: createWorkspaceStateBackend(this.workspace),
   *   loader: this.env.LOADER,
   * });
   * // sandbox: codemode.myTool() AND state.readFile() AND state.planEdits()
   * ```
   */
  state?: StateBackend;

  /**
   * Additional tool providers for the sandbox beyond the default tools and state.
   * Each provider adds a named namespace alongside `codemode.*` and `state.*`.
   */
  providers?: ToolProvider[];

  /**
   * The executor that runs the generated code.
   *
   * Use `DynamicWorkerExecutor` for Cloudflare Workers (requires a
   * `worker_loaders` binding in wrangler.jsonc), or implement the
   * `Executor` interface for other runtimes.
   *
   * If not provided, you must provide a `loader` instead.
   */
  executor?: Executor;

  /**
   * WorkerLoader binding for creating a `DynamicWorkerExecutor`.
   * This is a convenience alternative to passing a full `executor`.
   *
   * Requires `"worker_loaders": [{ "binding": "LOADER" }]` in wrangler.jsonc.
   */
  loader?: WorkerLoader;

  /**
   * Timeout in milliseconds for code execution. Defaults to 30000 (30s).
   * Only used when `loader` is provided (ignored if `executor` is given).
   */
  timeout?: number;

  /**
   * Controls outbound network access from sandboxed code.
   * - `null` (default): fetch() and connect() throw — sandbox is fully isolated.
   * - `undefined`: inherits parent Worker's network access.
   * - A `Fetcher`: all outbound requests route through this handler.
   *
   * Only used when `loader` is provided (ignored if `executor` is given).
   */
  globalOutbound?: Fetcher | null;

  /**
   * Custom tool description. Use `{{types}}` as a placeholder for the
   * auto-generated TypeScript type definitions of the available tools.
   */
  description?: string;
}

/**
 * Create a code execution tool that lets the LLM write and run JavaScript
 * with access to your tools in a sandboxed environment.
 *
 * The LLM sees typed `codemode.*` functions and writes code that calls them.
 * Code runs in an isolated Worker via `DynamicWorkerExecutor` — external
 * network access is blocked by default.
 *
 * Pass `state` to also expose the full `state.*` filesystem API alongside
 * `codemode.*`:
 *
 * @example
 * ```ts
 * import { createWorkspaceTools, createExecuteTool } from "@cloudflare/think";
 * import { createWorkspaceStateBackend } from "@cloudflare/shell";
 *
 * getTools() {
 *   const workspaceTools = createWorkspaceTools(this.workspace);
 *   const backend = createWorkspaceStateBackend(this.workspace);
 *   return {
 *     ...workspaceTools,
 *     execute: createExecuteTool({
 *       tools: myDomainTools,  // codemode.* — non-filesystem tools
 *       state: backend,        // state.* — full filesystem API
 *       loader: this.env.LOADER,
 *     }),
 *   };
 * }
 * ```
 *
 * @example Tools only (no filesystem in sandbox)
 * ```ts
 * createExecuteTool({
 *   tools: myTools,
 *   loader: this.env.LOADER,
 * });
 * ```
 *
 * @example Custom executor
 * ```ts
 * import { DynamicWorkerExecutor } from "@cloudflare/codemode";
 *
 * const executor = new DynamicWorkerExecutor({
 *   loader: this.env.LOADER,
 *   timeout: 60000,
 * });
 *
 * createExecuteTool({ tools: myTools, executor });
 * ```
 */
export function createExecuteTool(options: CreateExecuteToolOptions) {
  const { tools, description, state } = options;

  let executor: Executor;
  if (options.executor) {
    executor = options.executor;
  } else if (options.loader) {
    executor = new DynamicWorkerExecutor({
      loader: options.loader,
      timeout: options.timeout,
      globalOutbound: options.globalOutbound
    });
  } else {
    throw new Error(
      "createExecuteTool requires either an `executor` or a `loader` (WorkerLoader binding)."
    );
  }

  const providers: ToolProvider[] = [
    { tools }, // default "codemode" namespace
    ...(options.providers ?? [])
  ];
  if (state) {
    providers.push(stateToolsFromBackend(state));
  }

  return createCodeTool({ tools: providers, executor, description });
}