- 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 ServerUpdated
- 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 your first harness
chat() answers one request. A coding agent, a support bot, or a media studio needs more: one conversation that stays open, takes new messages while it works, and runs other agents from code. A harness gives you that. You define it once with the same options as chat(), then open a session for each conversation.
At the end of this page you have a session that answers prompts, queues and steers messages, runs a typed agent, and loads a plugin.
Warning
The harness is experimental. @tanstack/ai-harness and @tanstack/ai-harness-cli are on 0.x, and a minor release can change the API. Pin the exact version you install.
Install#
npm i @tanstack/ai @tanstack/ai-harness @tanstack/ai-persistence @tanstack/ai-openaipnpm add @tanstack/ai @tanstack/ai-harness @tanstack/ai-persistence @tanstack/ai-openaiyarn add @tanstack/ai @tanstack/ai-harness @tanstack/ai-persistence @tanstack/ai-openaibun add @tanstack/ai @tanstack/ai-harness @tanstack/ai-persistence @tanstack/ai-openaiWhich import to use#
Import each name from the entry that holds it. Only four entries load Node modules: plugins/node, plugins/coding, build, and worker. All other entries also load on an edge runtime. One exception at run time: the loopback helpers of oauth (startLoopbackReceiver and loopbackLogin) start a local Node server, so call them only on Node.
For your app and its routes:
| Import | Holds |
|---|---|
| @tanstack/ai-harness | defineHarness, createHarnessHost, definePlugin, durableTool, harnessText, oauthConnector, and the session and agent types. |
| @tanstack/ai-harness/server | createHarnessHandler and handleHarnessSocket. For your own transport: applyInput, parseHarnessInput, parseControlFrame, capabilitiesOf, and HARNESS_PROTOCOL_VERSION. |
| @tanstack/ai-harness/client | createHarnessClient, a typed client for a browser or another process. |
| @tanstack/ai-harness/view | createSessionView, the live state of a session for a UI. |
For plugins:
| Import | Holds |
|---|---|
| @tanstack/ai-harness/plugins | The first-party plugins that load no Node module: permissions, modelPicker, todos, goal, providerKeys, and more. |
| @tanstack/ai-harness/plugins/node | The first-party plugins that read files. Node only: agents, builtInAgents, fileCommands, projectInstructions, and boundToolOutput. |
| @tanstack/ai-harness/plugins/coding | The coding tools. Node only: workspaceTools, snapshots, and formatter. |
For helpers and builds:
| Import | Holds |
|---|---|
| @tanstack/ai-harness/oauth | The OAuth helpers: loopbackLogin, deviceLogin, exchangeCode, refreshCredential, and more. |
| @tanstack/ai-harness/media | The media helpers: mediaPart, mediaOfMessage, MediaError, and more. |
| @tanstack/ai-harness/build | buildHarness and artifactText, to ship a harness as a worker. Node only. |
| @tanstack/ai-harness/worker | runHarnessWorker, which runs a harness on stdin and stdout. Node only. |
1. Define the harness#
defineHarness takes the chat() options you know (adapter, systemPrompts, tools, middleware, subagents), plus a name. Importing it starts nothing.
import { defineAgent } from '@tanstack/ai'
import { defineHarness } from '@tanstack/ai-harness'
import { openaiText } from '@tanstack/ai-openai'
import { z } from 'zod'
const pricer = defineAgent({
name: 'pricer',
description: 'Looks up the price of a vendor plan',
inputSchema: z.object({ vendor: z.string() }),
run: async (ctx) => ({ vendor: ctx.input.vendor, cents: 1200 }),
})
export const assistant = defineHarness({
name: 'acme/assistant',
adapter: openaiText('gpt-6.1-sol'),
systemPrompts: ['You are a helpful assistant.'],
agents: [pricer],
})agents are agents you run from code. Put an agent in subagents instead when the model must call it as a tool, the same as in chat().
To ship the harness to users with no .env file, let each user connect their own key. See Connect model providers.
2. Open a session#
A host runs sessions. Give it the stores from @tanstack/ai-persistence, then open a session for a conversation id.
import { createHarnessHost } from '@tanstack/ai-harness'
import { memoryPersistence } from '@tanstack/ai-persistence'
const host = createHarnessHost({ persistence: memoryPersistence() })
const session = await host.open(assistant, { threadId: 'thread-1' })Opening the same threadId again gives another handle to the same live session. Release a handle with await session.close(), or open it with await using, so an idle session can close. See Lifecycle and retention.
Does the host serve many users? Give it access, so each user reaches only their own sessions. See Access and tenants.
To see every option of createHarnessHost, and which ones your app needs, see Host options.
3. Send a message#
send returns an operation. Await it for the text of the turn.
const turn = await session.send('Write a haiku about the sea.')
console.log(turn.text)The session saves the transcript, so the next message continues the same conversation.
Want the words as they arrive? Give the operation a listener for the event type you care about:
const next = session.send('Now one about the mountains.')
next.onEvent('TEXT_MESSAGE_CONTENT', ({ event }) => {
process.stdout.write(event.delta)
})
await nextThe events are standard AG-UI events, and event has the type you asked for. To listen to every operation of the session, see React to events.
4. Send messages while it works#
Users type while the agent works. Pick what a new message does:
- session.send(text): runs after the current turn. This is the default.
- session.send(text, { busy: 'reject' }): refuses the message while a turn runs.
- session.send(text, { mode: 'steer' }): adds the message to the running turn, before its next model call. The operation ends with that turn.
- session.send(text, { mode: 'followUp' }): runs after all current work.
Each call returns an operation. await operation.receipt resolves when the session stored the message.
const long = session.send('Plan a three-day trip to Lisbon.')
await session.send('Keep it under 500 euros.', { mode: 'steer' })
await longIf the turn makes no further model call, the steer message runs as the next turn.
5. Run an agent from code#
Every agent in agents and subagents is on session.agents. The input and the result are typed from the definition.
const price = await session.agents.pricer.run({ vendor: 'acme' })
console.log(price.cents)The session adds a short note about the result, or the error, to the transcript. The model sees it on the next turn. To run the agent in the background and start a new turn when it finishes or fails, use start:
session.agents.pricer.start({ vendor: 'globex' }, { startTurnWhenDone: true })6. Add a plugin#
A plugin adds tools, prompts, middleware, and agents to every session. setup runs once when the session opens. Resources you acquire close when the session closes.
import { toolDefinition } from '@tanstack/ai'
import { definePlugin } from '@tanstack/ai-harness'
const clock = definePlugin({
name: 'acme/clock',
setup: () => ({
prompts: [`Today is ${new Date().toDateString()}.`],
tools: [
toolDefinition({
name: 'current_time',
description: 'Get the current time',
}).server(async () => new Date().toISOString()),
],
}),
})
export const withClock = defineHarness({
name: 'acme/assistant-with-clock',
adapter: openaiText('gpt-6.1-sol'),
plugins: () => [clock],
})plugins is a function, so every session gets its own plugin instances. Set lifetime: 'turn' on a plugin to set it up again for each turn, or lifetime: 'host' to share one setup between all the sessions of the host.
If two plugins add a tool with the same name, host.open fails and names both plugins. To write your own, see Write a harness plugin.
Keep sessions across restarts#
Each host keeps every thread in a session log: the transcript, the inputs, the runs, the approvals that wait, the settings, and the plugin state. The host needs one store for it, log:
- log: the session log. Required.
- sessions: one index entry per thread, for a list of sessions. See List, rename, and fork sessions.
- metadata: data wider than one thread, for example the saved rules of permissions().
memoryPersistence() has every store, but it keeps them in memory only. To keep the log in your database, implement the LogStore contract. Then a turn continues after a crash or a deploy. See Durable sessions.
Before you run many servers or long threads, read the known limits.
Prompt caching#
Every session caches the stable start of its requests by default, with its threadId as the cache key. To change the default for every session, set promptCache on the harness:
export const longCache = defineHarness({
name: 'acme/long-cache',
adapter: openaiText('gpt-6.1-sol'),
promptCache: 'long',
})To change it for one session, pass promptCache to host.open. The session value wins over the harness value:
const support = await host.open(assistant, {
threadId: 'thread-2',
promptCache: { key: 'acme-support' },
})If the threadId already has a live session, host.open gives a handle to that session, which keeps its first promptCache value. The agents that the model calls as tools use the retention of the turn. session.usage() shows the cache reads and writes, see Track usage and cost. For the options and the cost, see Prompt Caching.
To change it for one prompt, pass overrides.promptCache. See Give one prompt its own settings.
Set a reasoning level#
A harness sends no reasoning level by default, so the main model thinks at the default of its provider. To set a level for every turn, set reasoning on the harness:
export const careful = defineHarness({
name: 'acme/careful',
adapter: openaiText('gpt-6.1-sol'),
reasoning: 'medium',
})reasoning is a level, or an object with these fields:
- level: off, minimal, low, medium, high, xhigh, or max. A model that does not have the level gets the nearest level that it has.
- summary: true streams the thinking text. false keeps it out of the stream.
- budgetTokens: optional. A thinking token budget, for models that think with a budget.
For the levels of each model, see Reasoning. To use another level for one prompt, pass overrides.reasoning. See Give one prompt its own settings.
What you have now#
- A harness defined once, with the same options as chat().
- A session that keeps the conversation and streams AG-UI events.
- Queue, steer, and follow-up for messages that arrive during a turn.
- Typed agents you run from code, in the foreground or the background.
- Plugins that add tools and prompts to each session.
Next: open Run agents from a harness to start agents from plugins and let the model call them as tools.