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.
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.
| 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.
Your repository has an AGENTS.md or a CLAUDE.md with its rules. projectInstructions({ root }) gives them to the model:
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.
title({ adapter }) and usage() write to the session index, one entry per thread:
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.
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.
Your agent can also give tasks to coding agents you already use. Delegate to coding agents shows how.
Next: let the user undo more than one turn with Undo a turn.