PAIPAI

codeModeTool()

A tool that runs a model-written script against an allowlist of your tools.

codeModeTool returns a PAI tool requiring the sandbox capability. The model calls it with { reason, js }. Its script runs in a fresh Node process with access to the sandbox filesystem and installed packages, and returns its value as the output's result, with what it printed as logs. Only application tools in the explicit allowlist are callable; they execute in the app. The sandbox image needs Node.js 22.13 or later. GKE blocks outbound network access; packages must already be installed in the image or workspace. There is no local execution fallback.

pnpm add @pai/code-mode @pai/sandbox @pai/sandbox-gke
import { codeModeTool } from "@pai/code-mode";

export const assistantAgent = defineAgent({
  // …
  tools: {
    code: codeModeTool({ tools: { listInvoices, listFirms } }),
  },
});

The model sees each enabled child declared as TypeScript, from the tool's existing AI SDK schema metadata: its description and each object property's description as doc comments, its input type and its result type. A child the agent also registers directly, under the same name, is declared by its result type alone, since the model has that tool's own description and input schema. PAI enforces the application-tool boundary.

When child tools are available, the description also tells the model to use scripts for known dependency chains whose values are fetched at runtime: await one result and compute the next call's inputs from it, without a model inference step between the calls. Only the declared children are callable.

The tool is sequential: in one model step, each script runs alone and in the order the model wrote it, after the calls before it, so a script can use a file written earlier in the same step.

Bind sandboxCapability({ provider }) in the agent's createPai registration, including for calculations with no child tools. Configure CPU and memory on the sandbox deployment. See the complete setup.

Import

import { codeModeTool } from "@pai/code-mode";

Signature

function codeModeTool(options?: {
  tools?: ToolMap;
  contextSchema?: z.ZodType;
  executionPolicy?: CodeModeExecutionPolicy;
}): ToolDefinition;

Parameters

options?: CodeModeToolOptions

Omit for calculations only. The script then gets no tools.

tools?: ToolMap

The allowlist. Keys are the names a script calls, as in tools.listInvoices(input). Each value is a defineTool definition or a registration. Entries are independent of the agent's own tool registrations.

tool: ToolDefinition

Needs execute. Cannot declare suspend or tools. Existing dataSchemas are allowed: nested ctx.data.write(...) is an asynchronous no-op without validation or chat emission, and ctx.data.list(...) returns []. The script receives the tool's returned data.

enabled?: boolean | ((ctx) => boolean | Promise<boolean>)

Resolved on each model step for the description, and once per code call for execution. Availability, not authorization.

Defaults to true.

mapContext?: (ctx) => toolContext

Project the code tool's context into this child's contextSchema.

onOutput?: (ctx) => void | Promise<void>

Observe this child's validated output.

contextSchema?: z.ZodType

The code tool's own context. enabled, mapContext, and onOutput receive it as runtimeContext. Without it, the context is the combined context of entries registered without mapContext, and mapped entries add nothing. Declare it when a callback reads anything else.

executionPolicy?: CodeModeExecutionPolicy

Per-script limits, exported from @pai/code-mode. Each value must be a positive integer; codeModeTool() throws a TypeError for an invalid value or unknown option. Time, source and result limits fail the whole call. Child input/output and total-count limits deny the affected child call; the script can catch that error. Requests beyond the execution capacity queue within the total-count budget and script deadline.

timeoutMs?: number

The whole script, including queued and running child calls.

Defaults to 30_000.

maxSourceBytes?: number

The script.

Defaults to 256 * 1024.

maxResultBytes?: number

JSON-encoded return value bytes and UTF-8 script-error bytes. The description advertises this budget. Oversized results and errors fail with short recovery feedback; successful JSON is never truncated. Logs keep only their last maxResultBytes. This is a per-invocation limit, not a conversation budget.

Defaults to 32 * 1024.

maxToolInputBytes?: number

One child call's input. Over it, that call rejects inside the script.

Defaults to 1024 * 1024.

maxToolOutputBytes?: number

One child call's output. Over it, that call rejects inside the script.

Defaults to 4 * 1024 * 1024.

maxBridgeRequests?: number

Total child requests per script, including queued calls. Further calls are denied.

Defaults to 256.

maxInFlightBridgeRequests?: number

Child calls executing at once. Further requests queue until capacity is available, within maxBridgeRequests and timeoutMs.

Defaults to 32.

Returns

A ToolDefinition.

Register on defineAgent({ tools }) under any key. The definition id is pai.code-mode, and the output type is CodeModeToolOutput. Several code tools with different allowlists can share an agent under different keys. Their generated descriptions start the same way, so say in instructions when to use each.

Model input

reason: string

A short, present-tense label for the person following along, such as "Totalling costs by category". It describes the purpose, not the code, so a UI can show it as the step label. Execution ignores it.

js: string

An async function body with top-level await and return. Node strips TypeScript annotations without type-checking; syntax such as enum that needs transformation fails. Use require(...) or await import(...) for Node built-ins and installed packages; static import declarations cannot appear in a function body. Relative file paths use the sandbox workspace. The return value is JSON-serialized.

Typed as CodeModeToolInput, exported from @pai/code-mode.

Tool output

result?: unknown

The script's JSON-serialized return value. Absent when it returned nothing.

logs?: string

What the script printed through console, process.stdout or process.stderr, in order, keeping the last maxResultBytes. Absent when it printed nothing. Subprocesses that inherit the script's stdio are not captured. A script that ends with an error, such as one it throws or a result over the limit, has no output: its error text ends with these logs instead. A timeout, an oversized source or a failure outside the script carries no logs.

logsTruncated?: true

true means earlier output was dropped, so logs holds only the last maxResultBytes. Absent otherwise.

Typed as CodeModeToolOutput, exported from @pai/code-mode. Code tool calls saved before logs existed hold the bare return value as their output. It does not match this shape, so loading such a call logs a stored_record_degraded warning and returns the value as stored: read output null-safely.

Examples

Calculations only

tools: { calculate: codeModeTool() },

Tighter limits

codeModeTool({
  tools: { listInvoices },
  executionPolicy: { timeoutMs: 10_000, maxResultBytes: 16 * 1024 },
});

A permission-gated child

codeModeTool({
  contextSchema: z.object({
    permissions: z.object({ canReadInvoices: z.boolean() }),
    workspaceId: z.string(),
    invoices: z.custom<InvoiceStore>(),
  }),
  tools: {
    listInvoices: {
      tool: listInvoices,
      enabled: ({ runtimeContext }) =>
        runtimeContext.permissions.canReadInvoices,
    },
  },
});

enabled hides the child from scripts. Enforce access inside listInvoices as well: see Code mode.

Learn more

On this page