Skip to content
Harness

Switch between agent profiles

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.

1. Add the plugin

ts
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.

2. Plan, then build

ts
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.')
  1. The plan agent reads the code and gives numbered steps. It has no tool that changes a file.
  2. The build agent changes the files, after your answer to each permission question.
  • session.runCommand({ name: 'agent' }) with no input shows the current agent and the list: Agent: build. Agents: build, plan.
  • A switch applies at the next turn.

The same switch is the agent setting. session.describe().config lists its options for a menu:

ts
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.

The built-in agents

AgentModeToolsWhat it does
buildprimary, the defaultEvery toolReads code, changes files, and runs commands.
planprimaryThe read tools and todo_writeReads the code and gives a plan in numbered steps.
generalsubagentEvery toolDoes a task in many steps and reports back.
exploresubagentThe read toolsSearches 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:

  • primary: it answers the user's turns. Pick it with the agent command or setting.
  • subagent: the main model starts it as a child.
  • all: both.

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.

Add your own agent

Give agents() a profile. This one reviews the changes with a stronger model:

ts
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:

FieldWhat it does
modelA 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.
systemText for the system prompt of each turn of this agent.
toolsThe tools that this agent can call, by name. A * matches any text, for example todo_*. Without it, the agent gets every tool.
permissionsPermission rules that count while this agent is the primary agent. At that time, they also apply in each subagent run.
stepsThe step limit. See Limit the steps.
hiddentrue leaves the agent out of the agent command and setting. The model can still start it as a subagent.
  • A profile replaces an earlier profile with the same name. The order is the built-in agents, then dirs, then agents. So a profile named plan replaces the built-in plan.
  • default names the primary agent of a new session. Without it, the first primary agent is the default, which is build.
  • In the main turn, a model that the thread stored with session.configure wins over the profile model. See What wins.
  • A tools list also filters the subagent tools. To let the agent start subagents, add their tool names, for example explore.

Load agents from Markdown files

Keep your agents next to your code as Markdown files. Save this file as .agents/agents/review.md:

markdown
---
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:

ts
export const fromFiles = agents({
  adapter: (model) => models[model] ?? sonnet,
  dirs: [`${root}/.agents/agents`],
})
  • The file name is the agent name, here review.
  • The body is the system prompt.
  • The frontmatter sets description, mode, model, tools, steps, and hidden: true. The default mode is all.
  • An unknown mode, or a steps that is not a whole number, stops host.open with an error, for example Agent file review.md: "mode" is not valid.
  • The plugin reads the folder when a session opens, and again at each session.reloadPlugins(). A folder that it cannot read gives no agents.

Limit the steps

An agent can call tools for a long time. steps sets the most model calls with tools in one run. With steps: 20:

  1. The agent makes up to 20 model calls with tools.
  2. Call 21 gets toolChoice: 'none' and a note: answer now, and say what is left to do.
  3. Then the run stops.

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:

  • The tool does not run.
  • The model gets this tool result: { "error": "Step limit reached. Answer without calling tools." }
  • The run stops. That turn can end with no text answer.

Let the model start a subagent

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:

  • The tools of the turn that started it, filtered by its tools.
  • Its system prompt.
  • Its model, or the model of the turn that started it.
  • Its step limit.

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.

Known limits

  • A subagent run uses the permissions of the primary agent, not the rules of its own profile. For example, when build is the primary agent and starts review, the build rules apply and the review rules do not. Put rules that must hold in every run in permissions({ rules }). See Rules in subagents.
  • A Markdown agent cannot set permissions. Put those rules in a profile in code.
  • On Amazon Bedrock, the model can still call a tool on the last call. The tool does not run, but the turn can end with no text answer.
  • A subagent run gets the tools and the model of its turn from the memory of the host that ran that turn. On another host, or after a restart, the run fails with Agent <name> cannot go on here. Start it again from a turn.
  • A subagent run that your code starts, not a turn, has no tools of a turn. Give its profile a model.

What you have now

  • A read-only plan agent, and a build agent to do the work.
  • Your own agents in code or in Markdown files, each with its own prompt, tools, model, and rules.
  • A step limit that ends a run with a text answer.
  • Subagents that the main model starts to search and to research.