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-gkeimport { 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.