- 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
Deploy a harness
Your harness works on your laptop. Now it needs to run next to your API, in its own process, on a different machine, or as a CLI your team can download. You pick the place. The harness definition stays the same.
| Place | How |
|---|---|
| Your server | createHarnessHost and createHarnessHandler in your routes. No build step. |
| Background workers | createHarnessWorker on a host that shares the log store. See Run background work on workers. |
| A worker process | buildHarness, then artifactText(dir) as the model of a chat() call. |
| Another machine | createHarnessHandler there, then harnessText({ url, token }) here. |
| A single executable | buildHarness({ compile }) with Bun. |
Run more than one server#
Give every server the same log store. Then any server can take a request for any thread:
- One server owns a thread at a time. An input that reaches another server waits in the log for the owner. See One host drives a thread.
- A worker runs the threads that no server has open: submitted inputs, schedules, and the work that a deploy cut.
- Set the same media: { secret } on the handler of each server, so each server checks the media URLs of the others. See Serve the session over HTTP.
- Stop each server with host.close({ recoverable: true }). A worker takes its cut work at once. See Stop a host for a deploy.
- If your agents use the coding tools on the disk of a server, set ownership: { hostId } on each host. Then a thread stays on the server that has its folder, while that server sends heartbeats. Use an id that stays the same after a restart, like the pod name of a StatefulSet, and keep a restart shorter than ownership.expiresAfterMs. See Move a thread to another server.
Build a worker artifact#
buildHarness bundles the harness with Bun and writes a manifest next to it. Install Bun on the build machine.
import { artifactText, buildHarness } from '@tanstack/ai-harness/build'
const { manifest } = await buildHarness({
entry: './src/studio.ts',
export: 'studio',
outDir: './dist/studio',
})
console.log(manifest.name, manifest.digest)- dist/studio/harness.js starts the harness as a worker. It reads session frames on stdin and writes them on stdout.
- dist/studio/harness.manifest.json names the harness, its agents and plugins, what it needs (file system, processes, network), and the sha256 digest of the bundle.
The build imports your entry in a child process to read the harness name, agents, and plugins. Importing a module runs its code.
Run the artifact as a model#
artifactText starts one worker process per outer thread and checks the digest first. A changed bundle is refused.
import { chat } from '@tanstack/ai'
const model = await artifactText('./dist/studio')
const stream = chat({
adapter: model,
messages: [{ role: 'user', content: 'Plan the launch post.' }],
threadId: 'thread-1',
})
// When you are done with the workers:
model.dispose()The worker keeps the conversation of its thread between calls. Workers get the environment of the process that starts them, so they can read your provider keys.
Use a harness on another machine#
On the other machine, serve the harness with createHarnessHandler. Check the bearer token in getUser of the host access. See Serve the session over HTTP.
Here, use it as a model:
import { harnessText } from '@tanstack/ai-harness'
const remote = harnessText({ url: 'http://build-box:8787', token: 'your-token' })The remote session keeps the conversation, so each call sends only the new message.
Ship a single executable#
Point compile.entry at the file that starts your app, for example a server or a command-line tool that imports the harness:
await buildHarness({
entry: './src/studio.ts',
export: 'studio',
outDir: './dist/studio',
compile: { entry: './src/main.ts', outfile: './dist/studio-app', target: 'bun-linux-x64' },
})Bun can build for another platform with target, for example bun-darwin-arm64 or bun-windows-x64.
Trace runs and report operations#
A turn is slow or fails on one of your servers, and you cannot see which run or which server. Give the host a tracer and an onOperation listener. The host then writes a span for each run, and tells your listener when each run starts and ends.
Pass an OpenTelemetry tracer as it is. Install @opentelemetry/api in your app first. It is an optional peer dependency of the harness.
import { trace } from '@opentelemetry/api'
import { createHarnessHost } from '@tanstack/ai-harness'
const host = createHarnessHost({
observability: {
tracer: trace.getTracer('support-app'),
onOperation: ({ phase, operation }) => {
if (phase !== 'settled') return
console.log(operation.kind, operation.status, operation.durationMs)
console.log('tokens', operation.usage?.totalTokens)
},
},
})The host writes these spans:
| Span | When |
|---|---|
| harness.operation | For each turn, agent run, and command that this host runs. A child agent run on the same host is a child span of the run that started it. |
| harness.recovery | This host takes the work of a host that stopped. |
| harness.wait | A wait for a person ends. The span starts at the time that the wait opened, also when another host opened it. |
| harness.transfer | host.transfer moves a conversation. |
Each span has harness.thread_id, and harness.tenant_id for a tenant. A harness.operation span also has harness.run_id, harness.parent_run_id, harness.operation.kind, harness.agent, and harness.operation.status.
onOperation gets one started report and one settled report for each operation:
- Only the host that runs the operation calls it. A server that only passed the input on reports nothing.
- A settled report has status, durationMs, and usage. The usage counts the model calls of the run, of its child runs, and of plugins in the run.
- A failed operation has error, with its message, and its code when it has one.
- If your listener throws, the host sends a harness.plugin.warning event and goes on.
Without observability, the host writes no spans and does no work for them.
Join a turn to the trace of your app#
Send the traceparent of your current span with the message:
import { context, propagation } from '@opentelemetry/api'
import { createHarnessClient } from '@tanstack/ai-harness/client'
const client = createHarnessClient({ url: '/api/harness', threadId: 'thread-1' })
const carrier: Record<string, string> = {}
propagation.inject(context.active(), carrier)
const traceparent = carrier.traceparent
await client.send('Where is my order?', traceparent ? { traceparent } : {})The client sends it as the traceparent header. The harness.operation span of the turn gets a link to your span, on the server that runs the turn. To send it from another language, see Traces.
Known limits#
Check these before you run many servers or long threads.
Across servers and restarts:
- A harnessAgent child waits only in memory. Its parent run waits on the same host. Another host, or a crash, does not resume it. After a crash, the next owner of the log ends the waits of the child.
- A log snapshot holds the full fold of the thread, so it grows with the thread. To keep the log small, drop the old records with keepLogRecords: 'since-snapshot'.
- host.resumePending opens the threads that it takes one at a time. A GET run by run id looks through the open sessions one at a time too.
Users and plugins:
- With an OAuth MCP server, the MCP SDK sends its own refresh request for each session. Only the save of the new token is single-flight, so two sessions can use a rotating refresh token at the same time.
- Plugin tools do not reach background agents. The question tool works only in a turn of the lead agent.
What you have now#
- The same harness in your server, on background workers, in a worker process, on another machine, or as a binary.
- A span and a report for each run, linked to the trace of your app.