branch:
tool.ts
6344 bytesRaw
import { tool, type Tool, asSchema } from "ai";
import { z } from "zod";
import type { ToolSet } from "ai";
import { generateTypes, type ToolDescriptors } from "./tool-types";
import type {
  Executor,
  ToolProvider,
  ToolProviderTools,
  ResolvedProvider
} from "./executor";
import { normalizeCode } from "./normalize";

const DEFAULT_DESCRIPTION = `Execute code to achieve a goal.

Available:
{{types}}

Write an async arrow function in JavaScript that returns the result.
Do NOT use TypeScript syntax — no type annotations, interfaces, or generics.
Do NOT define named functions then call them — just write the arrow function body directly.

Example: async () => { const r = await codemode.searchWeb({ query: "test" }); return r; }`;

export interface CreateCodeToolOptions {
  tools: ToolProviderTools | ToolProvider[];
  executor: Executor;
  /**
   * Custom tool description. Use {{types}} as a placeholder for the generated type definitions.
   */
  description?: string;
}

const codeSchema = z.object({
  code: z.string().describe("JavaScript async arrow function to execute")
});

type CodeInput = z.infer<typeof codeSchema>;
type CodeOutput = { code: string; result: unknown; logs?: string[] };

/**
 * Create a codemode tool that allows LLMs to write and execute code
 * with access to your tools in a sandboxed environment.
 *
 * Returns an AI SDK compatible tool.
 *
 * @example Raw tools (backwards compatible)
 * ```ts
 * createCodeTool({ tools: myToolSet, executor });
 * ```
 *
 * @example ToolProvider array with namespaces
 * ```ts
 * createCodeTool({
 *   tools: [
 *     { name: "github", tools: githubTools },
 *     { name: "state", tools: stateTools },
 *     { tools: aiTools }, // default "codemode" namespace
 *   ],
 *   executor,
 * });
 * ```
 */
function hasNeedsApproval(t: Record<string, unknown>): boolean {
  return "needsApproval" in t && t.needsApproval != null;
}

/**
 * Check if the tools option is an array of ToolProviders.
 * A plain ToolSet/ToolDescriptors is a Record (not an array).
 */
function isToolProviderArray(
  tools: ToolProviderTools | ToolProvider[]
): tools is ToolProvider[] {
  return Array.isArray(tools);
}

/**
 * Normalize the tools option into a list of ToolProviders.
 * Raw ToolSet/ToolDescriptors are wrapped as a single default provider.
 */
function normalizeProviders(
  tools: ToolProviderTools | ToolProvider[]
): ToolProvider[] {
  if (isToolProviderArray(tools)) {
    return tools;
  }
  return [{ tools }];
}

/**
 * Filter out tools with needsApproval and return a clean copy.
 */
function filterTools(tools: ToolProviderTools): ToolProviderTools {
  const filtered: Record<string, unknown> = {};
  for (const [name, t] of Object.entries(tools)) {
    if (!hasNeedsApproval(t as Record<string, unknown>)) {
      filtered[name] = t;
    }
  }
  return filtered as ToolProviderTools;
}

/**
 * Extract execute functions from tools, keyed by name.
 * Wraps each with schema validation when available.
 * Note: tool name sanitization happens in the executor, not here.
 */
function extractFns(
  tools: ToolProviderTools
): Record<string, (...args: unknown[]) => Promise<unknown>> {
  const fns: Record<string, (...args: unknown[]) => Promise<unknown>> = {};

  for (const [name, t] of Object.entries(tools)) {
    const execute =
      "execute" in t
        ? (t.execute as (args: unknown) => Promise<unknown>)
        : undefined;
    if (execute) {
      const rawSchema =
        "inputSchema" in t
          ? t.inputSchema
          : "parameters" in t
            ? (t as Record<string, unknown>).parameters
            : undefined;

      const schema = rawSchema != null ? asSchema(rawSchema) : undefined;

      fns[name] = schema?.validate
        ? async (args: unknown) => {
            const result = await schema.validate!(args);
            if (!result.success) throw result.error;
            return execute(result.value);
          }
        : execute;
    }
  }

  return fns;
}

/**
 * Resolve a ToolProvider into a ResolvedProvider ready for execution.
 * Filters out tools with `needsApproval`, validates schemas, and sanitizes names.
 */
/**
 * Wrap raw AI SDK tools into a ToolProvider under the default "codemode" namespace.
 *
 * @example
 * ```ts
 * createCodeTool({
 *   tools: [stateTools(workspace), aiTools(myTools)],
 *   executor,
 * });
 * ```
 */
export function aiTools(tools: ToolDescriptors | ToolSet): ToolProvider {
  return { tools };
}

export function resolveProvider(provider: ToolProvider): ResolvedProvider {
  const name = provider.name ?? "codemode";
  const filtered = filterTools(provider.tools);
  const resolved: ResolvedProvider = { name, fns: extractFns(filtered) };
  if (provider.positionalArgs) resolved.positionalArgs = true;
  return resolved;
}

export function createCodeTool(
  options: CreateCodeToolOptions
): Tool<CodeInput, CodeOutput> {
  const providers = normalizeProviders(options.tools);

  // Build type block and resolved providers for each provider.
  const typeBlocks: string[] = [];
  const resolvedProviders: ResolvedProvider[] = [];

  for (const provider of providers) {
    const name = provider.name ?? "codemode";
    const filtered = filterTools(provider.tools);
    const types =
      provider.types ?? generateTypes(filtered as ToolDescriptors, name);
    typeBlocks.push(types);
    resolvedProviders.push({ name, fns: extractFns(filtered) });
  }

  const typeBlock = typeBlocks.filter(Boolean).join("\n\n");

  const executor = options.executor;

  const description = (options.description ?? DEFAULT_DESCRIPTION).replace(
    "{{types}}",
    typeBlock
  );

  return tool({
    description,
    inputSchema: codeSchema,
    execute: async ({ code }) => {
      const normalizedCode = normalizeCode(code);

      const executeResult = await executor.execute(
        normalizedCode,
        resolvedProviders
      );

      if (executeResult.error) {
        const logCtx = executeResult.logs?.length
          ? `\n\nConsole output:\n${executeResult.logs.join("\n")}`
          : "";
        throw new Error(
          `Code execution failed: ${executeResult.error}${logCtx}`
        );
      }

      const output: CodeOutput = { code, result: executeResult.result };
      if (executeResult.logs) output.logs = executeResult.logs;
      return output;
    }
  });
}