- MCP Server ToolsUpdated
- Managed MCP with chat()Updated
- Manual MCPUpdated
- MCP Type GenerationUpdated
- MCP Client InputUpdated
- MCP Server Tools: AdvancedNew
- MCP EventsNew
- MCP AppsUpdated
- Serve Tools over HTTPUpdated
- Serve Resources and PromptsUpdated
- Ask for InputUpdated
- Draft Text from a ToolUpdated
- MCP Server TasksUpdated
- MCP Server AuthUpdated
- MCP Server SessionsUpdated
- MCP Server on stdioUpdated
Persistence (Advanced)
- GenerationsUpdated
- Realtime Voice ChatUpdated
- Text-to-SpeechUpdated
- TranscriptionUpdated
- Transcription: AdvancedNew
- Voice CreationUpdated
- Audio RecordingUpdated
- Audio GenerationUpdated
- Image GenerationUpdated
- Image Generation: AdvancedNew
- Video GenerationUpdated
- Video Generation: AdvancedNew
- Live GenerationUpdated
- World GenerationUpdated
- Generation HooksUpdated
- Build your first harnessUpdated
- Host optionsNew
- Send and show mediaUpdated
- Connect clientsUpdated
- Write a client in any languageNew
- Access and tenantsNew
- Run in the terminalUpdated
- Deploy a harnessUpdated
- Send inputs safelyUpdated
- Run a tool without a model
- Durable sessionsUpdated
- Run background work on workersNew
- List, rename, and fork sessionsUpdated
- Lifecycle and retentionNew
- Run side effects onceUpdated
- Wait for a personNew
- Build on the session logUpdated
- Control how a turn endsUpdated
- Store settings per threadUpdated
- Compact a long sessionUpdated
- Fork and reset a threadUpdated
- One log for a session treeUpdated
- Write a pluginUpdated
- Track usage and costUpdated
- Ask before risky tool callsUpdated
- Give your agent coding toolsUpdated
- Sandbox and extend the coding toolsUpdated
- Run the coding tools for many threadsNew
- Undo a turnUpdated
- Build a coding agentUpdated
- Delegate to coding agentsUpdated
- Switch between agent profilesUpdated
- Auth and connectorsUpdated
- Share a thread between peopleUpdated
- Connect model providersUpdated
- Build your own UIUpdated
- Code mode in a harnessUpdated
- Work until a goal is metUpdated
- Run agents from a harnessUpdated
- Hand off a conversationNew
- Add skillsUpdated
- Use MCP serversUpdated
- Give your agent a computerNew
- Use from any MCP clientUpdated
- Use a harness from an editor (ACP)Updated
- OverviewUpdated
- Quick StartUpdated
- ProvidersUpdated
- HarnessesUpdated
- Harness AuthUpdated
- WorkspaceUpdated
- ToolsUpdated
- PolicyUpdated
- Lifecycle & SnapshotsUpdated
- Portable SnapshotsUpdated
- Keep Files After ReloadUpdated
- Save a Named VersionUpdated
- Branch From a VersionUpdated
- Send a Frozen FileUpdated
- Let the Agent Save and ForkUpdated
- Pick Which Files to KeepUpdated
- What a Snapshot StoresUpdated
- Instance DurabilityUpdated
- Durable RunsUpdated
- EventsUpdated
Sandboxes (Advanced)
- Runtime ContextUpdated
- Bring Your Own KeyUpdated
- Keep Keys on the ServerNew
- Debug LoggingUpdated
- Multimodal ContentUpdated
- Files APIUpdated
- Per-Model Type SafetyUpdated
- Runtime Adapter SwitchingUpdated
- Tree-ShakingUpdated
- Extend AdapterUpdated
- Extend Adapter: AdvancedNew
- Typed Pre-Configured OptionsUpdated
- Test with a Fake ModelUpdated
- Prompt CachingUpdated
- Mid-Conversation ChangesUpdated
- Approval Flow ProcessingUpdated
Build a coding agent
You want your own coding agent in the terminal. It reads and edits files, and runs commands after you approve them. When a turn goes wrong, you undo it. The first-party plugins in @tanstack/ai-harness/plugins, @tanstack/ai-harness/plugins/node, and @tanstack/ai-harness/plugins/coding give you those parts. You pick the model and the rules.
1. Define the agent#
import { randomUUID } from 'node:crypto'
import { homedir } from 'node:os'
import { join } from 'node:path'
import { createInterface } from 'node:readline/promises'
import { createHarnessHost, defineHarness } from '@tanstack/ai-harness'
import type { RunAgentResumeItem } from '@tanstack/ai'
import {
compact,
modelPicker,
permissions,
title,
todos,
usage,
} from '@tanstack/ai-harness/plugins'
import { fileCommands, projectInstructions } from '@tanstack/ai-harness/plugins/node'
import { snapshots, workspaceTools } from '@tanstack/ai-harness/plugins/coding'
import { openaiText } from '@tanstack/ai-openai'
import { memoryPersistence } from '@tanstack/ai-persistence'
const root = process.cwd()
const smart = openaiText('gpt-6.1-sol')
const fast = openaiText('gpt-6-luna')
const coder = defineHarness({
name: 'acme/coder',
adapter: smart,
systemPrompts: ['You are a careful coding agent. Read before you edit.'],
plugins: () => [
permissions({ root }),
workspaceTools({ root }),
snapshots({ root, dataDir: join(homedir(), '.acme-coder', 'snapshots') }),
todos(),
modelPicker({ choices: { smart, fast }, default: 'smart' }),
fileCommands({ dir: `${root}/.claude/commands` }),
compact({ adapter: fast }),
title({ adapter: fast }),
usage(),
// Keep this plugin last. See "Project instructions" below.
projectInstructions({ root }),
],
})
const host = createHarnessHost({ persistence: memoryPersistence() })
const session = await host.open(coder, { threadId: 'main' })
const terminal = createInterface({
input: process.stdin,
output: process.stdout,
})
// Print the text of every turn, also of a turn that continues after an answer.
const stopPrinting = session.onEvent('TEXT_MESSAGE_CONTENT', ({ event }) => {
process.stdout.write(event.delta)
})
// A risky call stops the turn before it runs. Ask about each approval:
// once, always, or reject. Then send all the answers in one resolve.
async function answerApprovals() {
let waits = session.snapshot().pendingWaits
while (waits.length > 0) {
const resume: Array<RunAgentResumeItem> = []
for (const wait of waits) {
const answer = await terminal.question(`\n${wait.message ?? wait.kind} (once/always/reject) `)
resume.push({ interruptId: wait.waitId, status: 'resolved', payload: answer })
}
const inputId = randomUUID()
await session.resolve({ resume, inputId })
await session.settled({ inputId })
waits = session.snapshot().pendingWaits
}
}
// Type `exit` to stop.
let line = await terminal.question('\n> ')
while (line !== 'exit') {
if (line.startsWith('/')) {
// `/undo`, `/model fast`, `/mode plan`, and the other plugin commands.
const [name = '', ...args] = line.slice(1).split(' ')
console.log(await session.runCommand({ name, input: args.join(' ') || undefined }))
} else {
try {
await session.send(line)
await answerApprovals()
} catch (error) {
// A failed turn rejects. Show the error and keep the loop.
console.error(error)
}
}
line = await terminal.question('\n> ')
}
stopPrinting()
terminal.close()
await host.close()The plugins/node and plugins/coding entries run only in Node. The coding tools use the file system and the shell of the machine. A line that starts with / runs a command of a plugin. Any other line is a prompt.
2. Run it#
- Run npx tsx coder.ts in your project.
- Ask for a change. The agent reads files freely, and asks before it writes a file or runs a command.
- Answer once to allow the call, always to allow it from now on, or reject to refuse it.
- If you do not like the result, run /undo. The files go back to how they were before the turn.
What each plugin adds#
| Plugin | Adds |
|---|---|
| permissions({ root }) | Asks before risky tool calls. /mode switches between default, plan (read-only), acceptEdits (edits run without asking), and bypass. See Ask before risky tool calls. |
| workspaceTools({ root }) | Tools that read, search, and change the files in root, run commands, and read web pages. A path outside root is refused, or asks first with outside: 'ask'. See Give your agent coding tools. |
| snapshots({ root, dataDir }) | /undo puts the files back as they were before the last turn, and /redo brings them back. It needs git. See Undo a turn. |
| todos() | A todo_write tool the model uses for multi-step work, and /todos. |
| modelPicker({ choices }) | /model <name> switches the model at the next turn. |
| fileCommands({ dir }) | Each .md file becomes a slash command. $ARGUMENTS is replaced by what you type after it. |
| compact({ adapter }) | /compact replaces a long conversation with a summary. |
| title({ adapter }) | Names the session from its first message. |
| usage() | /usage shows the tokens and the cost of the session, with a line for each model. See Track usage and cost. |
| projectInstructions({ root }) | Adds AGENTS.md, CLAUDE.md, and facts about the environment to the system prompt. |
To compact on its own before the context limit, see Compact a harness session.
Project instructions#
Your repository has an AGENTS.md or a CLAUDE.md with its rules. projectInstructions({ root }) gives them to the model:
- It reads AGENTS.md and CLAUDE.md in each folder from the repository root (the folder with .git) down to root. Set other file names with files.
- global adds files that come first, for example ['~/.config/AGENTS.md'].
- When read_file reads a file in a subfolder, the model also gets the instruction files of the folders on the way, once each.
- An environment block gives the date, the platform, the working folder, and the git repository and branch. env: false turns it off.
- When you change an instruction file, the next turn sends the new text as a note.
Put projectInstructions() last in the plugin list. Then a changed file goes into the conversation as a mid-conversation change, and the prompt cache stays. When the prompt of another plugin comes after it, a change builds the system prompt again, and the prompt cache starts again.
Session titles and cost#
title({ adapter }) and usage() write to the session index, one entry per thread:
- title names the session from its first message. The turn does not wait for it.
- When usage knows the cost of the session, it writes the cost. A cost that the provider reports is used as it is.
- To price the calls of a provider that reports no cost, pass usage({ model }). Track usage and cost shows how.
A sidebar of sessions reads the title and the cost from the index. The usage() state also has contextTokens, the size of the lead model's context at its latest call, for a context meter.
Add more#
- agents({ adapter }): named agents with their own prompt, tools, and model. /agent plan switches to a read-only agent. See Switch between agent profiles.
- question(): a question tool. The model asks the user to pick an option, and waits for the answer.
- formatter({ root }): formats each file after a write. See Format each file after a write.
Add your own rules#
Tool plugins add permission rules to the PermissionRules extension point. Add your own for any tool:
import { PermissionRules } from '@tanstack/ai-harness/plugins'
import { definePlugin } from '@tanstack/ai-harness'
export const noDeploys = definePlugin({
name: 'acme/no-deploys',
setup: () => ({
contribute: [PermissionRules.item({ tool: 'deploy_*', decision: 'deny' })],
}),
})A trailing * matches every tool that starts with the text. The last matching rule wins. Ask before risky tool calls explains the rules, the modes, and the saved answers.
The workspace tools run on your machine with your permissions. For code that you do not trust, run the tools in a sandbox.
Hand work to Claude Code or Codex#
Your agent can also give tasks to coding agents you already use. Delegate to coding agents shows how.
What you have now#
- A terminal coding agent with file tools, approvals, modes, a todo list, and a model picker.
- /undo and /redo for the file changes of a turn.
- The rules of your repository in every turn. A change to them keeps the prompt cache.
- A title and a cost for each session.
Next: let the user undo more than one turn with Undo a turn.