Your agent has many read-only tools, and it calls them one at a time. Each call is a model round trip, and every tool schema goes into every request. With code mode, the model writes one TypeScript program that calls the tools it needs, and the program runs in an isolate. The read-only tools leave the tool list and become functions in that program. The coding tools can move too, and each of their calls keeps its permission rules.
npm i @tanstack/ai-code-mode @tanstack/ai-isolate-quickjspnpm add @tanstack/ai-code-mode @tanstack/ai-isolate-quickjsyarn add @tanstack/ai-code-mode @tanstack/ai-isolate-quickjsbun add @tanstack/ai-code-mode @tanstack/ai-isolate-quickjsimport { toolDefinition } from '@tanstack/ai'
import { codeMode } from '@tanstack/ai-code-mode/harness'
import { defineHarness } from '@tanstack/ai-harness'
import { createQuickJSIsolateDriver } from '@tanstack/ai-isolate-quickjs'
import { openaiText } from '@tanstack/ai-openai'
import { z } from 'zod'
const issues = [{ id: 'ENG-1', title: 'Fix login', state: 'open' }]
const listIssues = toolDefinition({
name: 'list_issues',
description: 'List the issues of the team',
inputSchema: z.object({ state: z.enum(['open', 'closed']) }),
}).server(async ({ state }) => issues.filter((issue) => issue.state === state))
const getIssue = toolDefinition({
name: 'get_issue',
description: 'Get one issue by id',
inputSchema: z.object({ id: z.string() }),
}).server(async ({ id }) => issues.find((issue) => issue.id === id) ?? null)
export const assistant = defineHarness({
name: 'acme/assistant',
adapter: openaiText('gpt-6.1-sol'),
tools: [listIssues, getIssue],
plugins: () => [codeMode({ driver: createQuickJSIsolateDriver() })],
})The model now has an execute_typescript tool. Inside the program, each moved tool is an external_* function, for example external_list_issues({ state: 'open' }).
driver takes any isolate driver. Change one line to run the programs somewhere else:
| Driver | Package | Runs in |
|---|---|---|
| createQuickJSIsolateDriver() | @tanstack/ai-isolate-quickjs | WebAssembly, on any runtime |
| createQuickJSBunIsolateDriver() | @tanstack/ai-isolate-quickjs-bun | Native QuickJS in Bun |
| createNodeIsolateDriver() | @tanstack/ai-isolate-node | A V8 isolate in Node |
| createCloudflareIsolateDriver() | @tanstack/ai-isolate-cloudflare | A Cloudflare Worker |
| createDaytonaIsolateDriver() | @tanstack/ai-isolate-daytona | A Daytona sandbox |
| createE2BIsolateDriver() | @tanstack/ai-isolate-e2b | An E2B sandbox |
Code mode isolates has the options of each driver.
By default, code mode moves only the tools whose calls need no question and no check:
Where the permission rules come from:
Every other tool stays a normal tool call, with its approvals.
Code mode also adds allow rules for execute_typescript and discover_tools, so a program itself does not ask, also with default: 'ask'. Each call in the program gets the rules of its own tool. Your own rules still win: { tool: 'execute_typescript', decision: 'ask' } asks before each program. With getSnippetBindings, code mode adds no rule, because a snippet runs code of its own.
To pick the tools yourself, pass include:
export const onlyIssues = codeMode({
driver: createQuickJSIsolateDriver(),
include: (tool) => tool.name.endsWith('_issue') || tool.name.endsWith('_issues'),
})The other options of createCodeMode pass through: timeout, memoryLimit, lazyToolsConfig, and more.
The prompt of code mode lists each moved tool with its full TypeScript type. With 100 tools, that is a lot of tokens on every turn, and most turns use a few tools. Set lazy: true, and the prompt lists only the names:
export const lazyCodeMode = codeMode({
driver: createQuickJSIsolateDriver(),
lazy: true,
lazyToolsConfig: { includeDescription: 'first-sentence' },
})The model then works in three steps:
includeDescription sets what the prompt shows with each name: 'none' (the default, the name only), 'first-sentence', or 'full'. Tools that stay outside code mode keep their full schema.
To fix a bug, the model reads a file, edits it, and runs the tests. Each step is a tool call and a model round trip. Move the coding tools into code mode, and one program does all the steps. Each call in the program still gets the permission rules of its tool.
import { codeMode } from '@tanstack/ai-code-mode/harness'
import { createHarnessHost, defineHarness } from '@tanstack/ai-harness'
import { permissions } from '@tanstack/ai-harness/plugins'
import { workspaceTools } from '@tanstack/ai-harness/plugins/coding'
import { createQuickJSIsolateDriver } from '@tanstack/ai-isolate-quickjs'
import { openaiText } from '@tanstack/ai-openai'
import { memoryPersistence } from '@tanstack/ai-persistence'
const root = process.cwd()
export const coder = defineHarness({
name: 'acme/coder',
adapter: openaiText('gpt-6.1-sol'),
plugins: () => [
permissions({ root }),
workspaceTools({ root }),
codeMode({ driver: createQuickJSIsolateDriver(), include: () => true }),
],
})include: () => true picks every tool of this harness. Mount codeMode() after workspaceTools(). Then the program gets the same edit tool as the model: editStyle picks edit_file or patch. With the default 'auto', a GPT model gets patch.
Each external_* call of a coding tool gets the decision of a direct call, for the paths and the commands of the call:
The prompt of code mode tells the model that these calls can wait and can throw.
A call that asks opens a wait of kind question, with the schema of an approval. The turn does not stop while the program waits. Answer the question with session.answer:
const host = createHarnessHost({ persistence: memoryPersistence() })
const session = await host.open(coder, { threadId: 'main' })
const turn = session.send('Run the tests, then fix the first failure.')
const wait = await session.waitForRun({ runId: turn.id })
// wait.kind is 'question'. wait.message is, for example:
// Allow bash {"command":"pnpm test"}? Answer once, always, or reject.
await session.answer({ questionId: wait.waitId, value: 'once' })In a UI, the question is an item of questions in a session view. In the CLI, type the answer.
import { createSessionView } from '@tanstack/ai-harness/view'
const view = createSessionView(session)
// Your buttons call this with 'once', 'always', or 'reject'.
async function answerQuestion(answer: 'once' | 'always' | 'reject') {
const [question] = view.store.get().questions
await question?.answer(answer)
}A program has 30 seconds by default. With a coding tool in it, the default is 10 minutes, because the wait for the user counts. Set timeout in milliseconds to change it. When the time runs out, the program stops, and its open question closes.
With a coding tool, execute_typescript is a durable tool. The session log keeps the result of each finished call of a coding tool. After a host stop, the next host runs the program again: a finished call returns its kept result, and the same question waits. If the stop cut an edit or a command while it ran, that call throws and does not run again.
A moved tool leaves the tool list of the model. Then the model needs a program even for one small call. Set directCalls: 'keep', and the model can call each moved tool directly too:
export const keepDirect = codeMode({
driver: createQuickJSIsolateDriver(),
directCalls: 'keep',
})The plugin chooses the tools again before every turn. A plugin that finds tools at run time with discoverTools (for example after a sign-in) adds them first, and code mode picks from them on the next turn. Tool names that are not JavaScript identifiers, for example notion-search, get a safe name inside the program: external_notion_search. When two tools get the same safe name, only the first one moves. The other one stays a normal tool call.