Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1cdc116
feat(ai-harness): first-party plugins in @tanstack/ai-harness/plugins
AlemTuzlak Oct 13, 2026
84c6153
docs(ai-harness): say that a tool with no rule runs in plan mode
AlemTuzlak Oct 13, 2026
d323e93
Merge remote-tracking branch 'origin/feat/harness-core' into feat/har…
AlemTuzlak Oct 13, 2026
a894987
feat(ai-harness): coding tools in @tanstack/ai-harness/plugins/coding
AlemTuzlak Oct 13, 2026
cee77b5
Merge remote-tracking branch 'origin/feat/harness-core' into feat/har…
AlemTuzlak Oct 13, 2026
0d6ec34
Merge remote-tracking branch 'origin/feat/harness-plugins' into feat/…
AlemTuzlak Oct 13, 2026
4d87928
docs(ai-harness): end the terminal loop of the coding samples on exit
AlemTuzlak Oct 13, 2026
eb69ad0
Merge remote-tracking branch 'origin/feat/harness-core' into feat/har…
AlemTuzlak Oct 14, 2026
9bdd860
fix(ai-harness): adopt object params from #1779
AlemTuzlak Oct 14, 2026
b113c45
docs(ai-harness): answer permission questions with onEvent
AlemTuzlak Oct 14, 2026
aea1f8a
ci: apply automated fixes
autofix-ci[bot] Oct 14, 2026
4218616
Merge remote-tracking branch 'origin/feat/harness-plugins' into feat/…
AlemTuzlak Oct 14, 2026
1a1a7db
fix(ai-harness, ai-e2e): adopt object params from #1779
AlemTuzlak Oct 14, 2026
b33f052
docs(ai-harness): print and answer with onEvent in the coding samples
AlemTuzlak Oct 14, 2026
5225695
docs(ai-harness): answer each open permission question once
AlemTuzlak Oct 14, 2026
114781d
Merge remote-tracking branch 'origin/feat/harness-plugins' into feat/…
AlemTuzlak Oct 14, 2026
332a8fc
docs(ai-harness): ask one question at a time in the coding samples
AlemTuzlak Oct 14, 2026
7ae9f5c
feat(ai-harness, ai-sandbox): codingAgents and sandboxWorkspaceBacken…
AlemTuzlak Oct 15, 2026
e2a239a
Merge remote-tracking branch 'origin/feat/harness-core' into feat/har…
AlemTuzlak Oct 15, 2026
cda1737
Merge remote-tracking branch 'origin/feat/harness-core' into feat/har…
AlemTuzlak Oct 16, 2026
c75aefd
fix(ai-harness, ai-sandbox, ai-e2e): use session.send() after the cor…
AlemTuzlak Oct 16, 2026
3ede1d9
Merge remote-tracking branch 'origin/feat/harness-core' into feat/har…
AlemTuzlak Oct 16, 2026
e9ff045
fix(ai-harness, ai-sandbox): review fixes for the coding tools
AlemTuzlak Oct 16, 2026
d428788
perf(ai-harness): faster file reads, edits, and searches in the codin…
AlemTuzlak Oct 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/ai-harness-coding-tools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@tanstack/ai-harness': minor
---

Add coding tools in the new Node entry `@tanstack/ai-harness/plugins/coding`:

- `workspaceTools({ root })` gives `read_file` (text pages, images, PDFs), `write_file`, `edit_file` or `patch` (`editStyle`), `list_files`, `grep`, `bash` (with `background`), `webfetch`, and `websearch` (with a `SearchProvider`). With `permissions()`, edits, `bash`, and `webfetch` ask first, and the rules also apply to the real path of a link. `outside: 'ask'` asks before a path outside `root`. The `cwd` thread setting gives each thread its own working folder.
- A `WorkspaceBackend` runs the tools. `hostBackend` is the default. On Windows, commands do not run a program from the working folder by its name.
- `snapshots()` adds `/undo` and `/redo`, which restore only the files of the turn (git 2.26 or newer), file restore for `session.revert()`, and a diff for each model step.
- `formatter()` formats each written file with the formatter of the project. With `permissions()`, each run asks first.
- `WorkspaceHooks` lets other plugins run code after a read or a write.

`@vscode/ripgrep` and `turndown` are optional peer dependencies.
10 changes: 10 additions & 0 deletions .changeset/ai-sandbox-harness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@tanstack/ai-sandbox': minor
---

Add `@tanstack/ai-sandbox/harness` for `@tanstack/ai-harness`:

- `codingAgents({ sandbox, agents, workspace? })` is a harness plugin. The lead model gets one tool per coding agent (Claude Code, Codex, Grok Build, or any ACP agent). Each agent works in the sandbox, keeps its own session per thread (also after a restart), and starts read-only in the `plan` mode of `permissions()`. `/fresh [agent]` starts a new session. `workspace: 'shared'` (default) runs the agents of a thread one at a time in one sandbox. `'per-agent'` gives each agent its own sandbox.
- `sandboxWorkspaceBackend(sandbox)` runs the coding tools of `@tanstack/ai-harness/plugins/coding` (`workspaceTools()`, `snapshots()`, `formatter()`) in a sandbox.

`@tanstack/ai-harness` is an optional peer dependency.
29 changes: 29 additions & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -1030,6 +1030,35 @@
"addedAt": "2026-10-13",
"updatedAt": "2026-10-16"
},
{
"label": "Give your agent coding tools",
"to": "harness/coding-tools",
"addedAt": "2026-10-13",
"updatedAt": "2026-10-14"
},
{
"label": "Sandbox and extend the coding tools",
"to": "harness/coding-tools-backends",
"addedAt": "2026-10-13",
"updatedAt": "2026-10-16"
},
{
"label": "Undo a turn",
"to": "harness/snapshots",
"addedAt": "2026-10-13",
"updatedAt": "2026-10-13"
},
{
"label": "Build a coding agent",
"to": "harness/coding-agent",
"addedAt": "2026-10-13",
"updatedAt": "2026-10-14"
},
{
"label": "Delegate to coding agents",
"to": "harness/coding-agents",
"addedAt": "2026-10-13"
},
{
"label": "Switch between agent profiles",
"to": "harness/agents",
Expand Down
200 changes: 200 additions & 0 deletions docs/harness/coding-agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
---
title: Build a coding agent
id: harness-coding-agent
order: 6
description: "Turn a harness into a coding agent with file tools, permission modes, undo, a todo list, a model picker, project instructions, session titles, a cost count, and /compact."
keywords:
- tanstack ai
- harness
- coding agent
- permissions
- workspace tools
- project instructions
- undo
---

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` and `@tanstack/ai-harness/plugins/coding` give you those parts. You pick the model and the rules.

## 1. Define the agent

```ts group=harness-coding-agent
import { homedir } from 'node:os'
import { join } from 'node:path'
import { createInterface } from 'node:readline/promises'
import {
HARNESS_EVENTS,
createHarnessHost,
defineHarness,
} from '@tanstack/ai-harness'
import {
compact,
fileCommands,
modelPicker,
permissions,
projectInstructions,
title,
todos,
usage,
} from '@tanstack/ai-harness/plugins'
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,
})

// A risky call waits for an answer: once, always, or reject.
// Calls can run in parallel. Ask one question at a time, each once.
const asked = new Set<string>()
let asking = Promise.resolve()
const stopAsking = session.onEvent('CUSTOM', ({ event }) => {
if (event.name !== HARNESS_EVENTS.question) return
asking = asking.then(async () => {
for (const question of session.snapshot().pendingQuestions) {
if (asked.has(question.questionId)) continue
asked.add(question.questionId)
const answer = await terminal.question(`\n${question.message} `)
await session.answer({ questionId: question.questionId, value: answer })
}
})
})

// 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.command(name, args.join(' ') || undefined))
} else {
const turn = session.send(line)
turn.onEvent('TEXT_MESSAGE_CONTENT', ({ event }) => {
process.stdout.write(event.delta)
})
try {
await turn
} catch (error) {
// A failed turn rejects. Show the error and keep the loop.
console.error(error)
}
}
line = await terminal.question('\n> ')
}
stopAsking()
terminal.close()
await host.close()
```

Both plugin 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

1. Run `npx tsx coder.ts` in your project.
2. Ask for a change. The agent reads files freely, and asks before it writes a file or runs a command.
3. Answer `once` to allow the call, `always` to allow it from now on, or `reject` to refuse it.
4. 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](./permissions). |
| `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](./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](./snapshots). |
| `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](./usage). |
| `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](./compaction).

## 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](../advanced/mid-conversation-changes), 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](./usage#show-usage-and-price-the-calls) shows how.

A [sidebar of sessions](./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](./agents).
- `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](./coding-tools-backends#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:

```ts group=harness-coding-agent
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](./permissions) 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](./coding-tools-backends#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](./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](./snapshots).
129 changes: 129 additions & 0 deletions docs/harness/coding-agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
---
title: Delegate to coding agents
id: harness-coding-agents
order: 11
description: "Let a harness hand coding work to Claude Code, Codex, Grok Build, or any ACP agent. Each one works in a sandbox and keeps its own session."
keywords:
- tanstack ai
- harness
- claude code
- codex
- grok build
- sandbox
- subagents
---

Your lead agent plans the work, but you want Claude Code or Codex to do the edits, because they are good at code and you already use them. `codingAgents` gives the lead model one tool per coding agent. Each agent works in a sandbox and keeps its own session between calls.

## 1. Add the plugin

<!-- ::start:tabs variant="package-manager" mode="install" -->

react: @tanstack/ai-harness @tanstack/ai-sandbox @tanstack/ai-sandbox-local-process @tanstack/ai-claude-code @tanstack/ai-codex
vue: @tanstack/ai-harness @tanstack/ai-sandbox @tanstack/ai-sandbox-local-process @tanstack/ai-claude-code @tanstack/ai-codex
solid: @tanstack/ai-harness @tanstack/ai-sandbox @tanstack/ai-sandbox-local-process @tanstack/ai-claude-code @tanstack/ai-codex
svelte: @tanstack/ai-harness @tanstack/ai-sandbox @tanstack/ai-sandbox-local-process @tanstack/ai-claude-code @tanstack/ai-codex
preact: @tanstack/ai-harness @tanstack/ai-sandbox @tanstack/ai-sandbox-local-process @tanstack/ai-claude-code @tanstack/ai-codex
angular: @tanstack/ai-harness @tanstack/ai-sandbox @tanstack/ai-sandbox-local-process @tanstack/ai-claude-code @tanstack/ai-codex
octane: @tanstack/ai-harness @tanstack/ai-sandbox @tanstack/ai-sandbox-local-process @tanstack/ai-claude-code @tanstack/ai-codex
vanilla: @tanstack/ai-harness @tanstack/ai-sandbox @tanstack/ai-sandbox-local-process @tanstack/ai-claude-code @tanstack/ai-codex

<!-- ::end:tabs -->

```ts group=harness-coding-agents
import { defineHarness } from '@tanstack/ai-harness'
import { permissions } from '@tanstack/ai-harness/plugins'
import { claudeCodeText } from '@tanstack/ai-claude-code'
import { codexText } from '@tanstack/ai-codex'
import { openaiText } from '@tanstack/ai-openai'
import { defineSandbox, defineWorkspace, localSource } from '@tanstack/ai-sandbox'
import { codingAgents } from '@tanstack/ai-sandbox/harness'
import { localProcessSandbox } from '@tanstack/ai-sandbox-local-process'

const repo = '/path/to/your/repo'

export const lead = defineHarness({
name: 'acme/lead',
adapter: openaiText('gpt-6.1-sol'),
plugins: () => [
permissions(),
codingAgents({
sandbox: defineSandbox({
id: 'repo',
provider: localProcessSandbox({ dir: repo }),
workspace: defineWorkspace({ source: localSource(repo) }),
}),
agents: {
claude_code: {
adapter: claudeCodeText('claude-opus-4-8', { permissionMode: 'acceptEdits' }),
description: 'Larger changes, refactors, and reviews',
},
codex: {
adapter: codexText('gpt-5.3-codex', { sandboxMode: 'workspace-write' }),
description: 'Quick fixes and tests',
},
},
}),
],
})
```

`localProcessSandbox({ dir: repo })` works in your repo folder itself, so every thread and every agent shares that folder. Leave out `dir` to give each sandbox its own copy of the repo.

The lead model now has a `claude_code` tool and a `codex` tool. Each tool takes one `task`: the whole job, in words. The agent sees only that text and the files in its sandbox.

## 2. Ask for work

Open a session, and give the lead a task for both agents:

```ts group=harness-coding-agents
import { createHarnessHost } from '@tanstack/ai-harness'
import { memoryPersistence } from '@tanstack/ai-persistence'

const host = createHarnessHost({ persistence: memoryPersistence() })
const session = await host.open(lead, { threadId: 'date-parser' })

const turn = await session.send(
'Have claude_code add a test for the date parser, then have codex fix what fails.',
)
console.log(turn.text)
```

The lead calls `claude_code` with a task, and gets back its answer. Then it calls `codex`. Each agent works in the same sandbox, one after the other.

## Sessions

Each agent keeps its own session per harness thread. The next task for `claude_code` resumes the same Claude Code session, so it still knows the files it read. The session ids live in the plugin state, so they also survive a restart of the host.

To start over, run `/fresh claude_code`, or `/fresh` for every agent.

## Workspaces

`workspace` chooses where the agents work:

| Value | Where each agent works | At the same time |
| --- | --- | --- |
| `'shared'` (default) | One sandbox per harness thread | One agent at a time |
| `'per-agent'` | A sandbox per agent | Yes |

Use `'shared'` when the agents build on each other's changes. Use `'per-agent'` when they work on separate copies and you merge the results.

## Plan mode

When the `permissions()` plugin is in [`plan` mode](./permissions#pick-a-mode), the agents start read-only:

- Claude Code gets `permissionMode: 'plan'`.
- Codex gets `sandboxMode: 'read-only'`.
- Any other agent gets its `planModelOptions`, for example `{ permissionMode: 'default' }` for an ACP agent that asks before each edit.
- An agent with no `planModelOptions` does not run in `plan` mode. Its tool call gets an error.

## Other agents and sandboxes

- `adapter` takes any coding-agent adapter: `grokBuildText` from `@tanstack/ai-grok-build`, or `acpCompatibleText` from `@tanstack/ai-acp` for any ACP agent.
- `sandbox` takes any sandbox provider. [Sandbox providers](../sandbox/providers) lists them.
- `modelOptions` on an agent is added to every call, for example a fixed `permissionMode`.

## What you have now

- A lead model that hands tasks to Claude Code and Codex.
- One sandbox per thread, and one saved session per agent.
Loading
Loading