Your agent has two jobs. First it must plan a change without touching a file. Then it must build the change with every tool. The main model also needs a fast helper that searches the code. agents() gives the harness named agent profiles. Each profile has its own system prompt, tools, model, permission rules, and step limit.
import { readFile, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { toolDefinition } from '@tanstack/ai'
import { createHarnessHost, defineHarness } from '@tanstack/ai-harness'
import { permissions } from '@tanstack/ai-harness/plugins'
import { agents } from '@tanstack/ai-harness/plugins/node'
import { anthropicText } from '@tanstack/ai-anthropic'
import { memoryPersistence } from '@tanstack/ai-persistence'
import { z } from 'zod'
const root = process.cwd()
const sonnet = anthropicText('claude-sonnet-5-5')
const readTool = toolDefinition({
name: 'read_file',
description: 'Read a file in the project',
inputSchema: z.object({ path: z.string() }),
}).server(({ path }) => readFile(resolve(root, path), 'utf8'))
const writeTool = toolDefinition({
name: 'write_file',
description: 'Write a file in the project',
inputSchema: z.object({ path: z.string(), content: z.string() }),
}).server(async ({ path, content }) => {
await writeFile(resolve(root, path), content)
return `Wrote ${path}.`
})
const coder = defineHarness({
name: 'acme/coder',
adapter: sonnet,
tools: [readTool, writeTool],
plugins: () => [
permissions({ root, rules: [{ tool: 'write_file', decision: 'ask', kind: 'edit' }] }),
agents({ adapter: () => sonnet }),
],
})adapter turns a model id into an adapter. Here every agent uses the same model.
const host = createHarnessHost({ persistence: memoryPersistence() })
const session = await host.open(coder, { threadId: 'dark-mode' })
await session.runCommand({ name: 'agent', input: 'plan' })
const plan = await session.send('How do I add a dark mode?')
console.log(plan.text)
await session.runCommand({ name: 'agent', input: 'build' })
await session.send('Do step 1.')The same switch is the agent setting. session.describe().config lists its options for a menu:
const setting = session.describe().config.find((entry) => entry.key === 'agent')
if (setting?.option.type === 'select') {
console.log(setting.value, setting.option.options) // 'build' ['build', 'plan']
}
await session.setPluginSetting({ key: 'agent', value: 'plan' })A client can change the setting only when the harness exposes it. Add expose: { config: ['agent'] } to defineHarness.
| Agent | Mode | Tools | What it does |
|---|---|---|---|
| build | primary, the default | Every tool | Reads code, changes files, and runs commands. |
| plan | primary | The read tools and todo_write | Reads the code and gives a plan in numbered steps. |
| general | subagent | Every tool | Does a task in many steps and reports back. |
| explore | subagent | The read tools | Searches the code and reports the paths that it finds. |
The read tools are read_file, list_files, grep, webfetch, websearch, and question. An agent gets only the tools that the harness has.
The mode of a profile says who starts it:
The plan agent and the plan mode of permissions are separate. The agent has only the read tools and todo_write. The mode denies edits and commands for every agent. To drop the built-in agents, set builtIns: false.
Give agents() a profile. This one reviews the changes with a stronger model:
import type { AnyTextAdapter } from '@tanstack/ai'
import type { AgentProfile } from '@tanstack/ai-harness/plugins/node'
const review: AgentProfile = {
name: 'review',
description: 'Reviews the changes and lists the risks. Does not change files.',
mode: 'all',
model: 'claude-opus-5-5',
system: 'You review code. Read the diff, then list each risk with its file and line.',
tools: ['read_file', 'bash'],
permissions: [{ tool: 'bash', resource: 'git diff*', decision: 'allow' }],
steps: 20,
}
const models: Record<string, AnyTextAdapter> = {
'claude-opus-5-5': anthropicText('claude-opus-5-5'),
}
export const withReview = agents({
adapter: (model) => models[model] ?? sonnet,
agents: [review],
})name, description, and mode are required. The other fields:
| Field | What it does |
|---|---|
| model | A model id. adapter turns it into an adapter. Without it, a primary agent keeps the harness model, and a subagent uses the model of the turn that started it. |
| system | Text for the system prompt of each turn of this agent. |
| tools | The tools that this agent can call, by name. A * matches any text, for example todo_*. Without it, the agent gets every tool. |
| permissions | Permission rules that count while this agent is the primary agent. At that time, they also apply in each subagent run. |
| steps | The step limit. See Limit the steps. |
| hidden | true leaves the agent out of the agent command and setting. The model can still start it as a subagent. |
Keep your agents next to your code as Markdown files. Save this file as .agents/agents/review.md:
---
description: Reviews the changes and lists the risks. Does not change files.
mode: all
model: claude-opus-5-5
tools: [read_file]
steps: 20
---
You review code. Read the diff, then list each risk with its file and line.Then give agents() the folder:
export const fromFiles = agents({
adapter: (model) => models[model] ?? sonnet,
dirs: [`${root}/.agents/agents`],
})An agent can call tools for a long time. steps sets the most model calls with tools in one run. With steps: 20:
The limit counts again in each turn of a primary agent and in each run of a subagent.
Some providers do not obey 'none'. On Amazon Bedrock, 'none' after a tool call goes to the model as auto. See Provider notes. If the model calls a tool on the last call anyway:
A profile with the mode subagent or all is also a subagent. The main model gets a tool for each one, with the name of the agent, for example explore. Ask the build agent: use explore to find where the theme is set.
A subagent run gets these parts of its profile:
A subagent run stops after 50 model calls, or when an answer is cut at the output limit. The harness also limits the depth and the number of children. See Stay within limits. To give one run from code its own budget and time limit, see Give one run a budget and a time limit.