- 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
Host options
createHarnessHost has many options, and their names do not always tell you if you need them. Most apps need only one or two. A local app, like a coding agent on your own computer, needs none.
The host is the part of your server that runs the sessions. A session is one conversation with your agent. The host keeps the state of each session, runs its turns, and gives clients its events.
Start with no options#
On your own computer, with one user, create the host with no options:
import { createHarnessHost, defineHarness } from '@tanstack/ai-harness'
import { openaiText } from '@tanstack/ai-openai'
const assistant = defineHarness({
name: 'acme/assistant',
adapter: openaiText('gpt-6.1-sol'),
})
const host = createHarnessHost()
await using session = await host.open(assistant, { threadId: 'my-first-chat' })
const turn = await session.send('Hello!')
console.log(turn.text)This host keeps everything in memory. When the process stops, the sessions are gone. At the start, the host writes one warning about this to its logger (the console by default). There is no login check, so every call acts for the one local user.
Find the options that you need#
Find your situation below. Add only the options of that situation.
Most servers need these two first:
- The sessions must stay after a restart. Add persistence. See Durable sessions.
- More than one person uses the app. Add access. Each call then needs the user that it acts for. See Access and tenants.
The other options are for some apps only:
- You run more than one server. Give every server the same persistence. If your agents use files on the disk of a server, add ownership. See Deploy a harness.
- Users bring their own API keys. Add credentials, so the keys stay after a restart. See Where the keys are stored.
- You want traces and metrics. Add observability. See Trace runs and report operations.
- You want the warnings in your own logger. Add logger, for example in a full-screen terminal UI. See Send the warnings to your own logger.
A server for many users usually starts like this:
import { createHarnessHost, membersOnly } from '@tanstack/ai-harness'
import { memoryPersistence } from '@tanstack/ai-persistence'
const host = createHarnessHost({
// Use a store on your own database in production.
persistence: memoryPersistence(),
access: {
// Your own login check. Return null when nobody is logged in.
getUser: async ({ request }) => {
const id = request.headers.get('x-user-id')
return id ? { id } : null
},
rules: membersOnly(),
},
})Send the warnings to your own logger#
By default, the harness writes its warnings and errors to the console. In a full-screen terminal UI, these lines break the screen. On a server, they do not match the format of your other logs. Give the host your own logger. This one writes each line to a file:
import { appendFileSync } from 'node:fs'
import { inspect } from 'node:util'
import { createHarnessHost } from '@tanstack/ai-harness'
// Write each line to a file, so the terminal UI keeps its screen.
function toFile(level: string, message: string, meta?: Record<string, unknown>) {
// An error in the meta prints with its stack.
const details = meta ? ` ${inspect(meta)}` : ''
appendFileSync('harness.log', `${new Date().toISOString()} ${level} ${message}${details}\n`)
}
const host = createHarnessHost({
logger: {
debug: (message, meta) => toFile('debug', message, meta),
info: (message, meta) => toFile('info', message, meta),
warn: (message, meta) => toFile('warn', message, meta),
error: (message, meta) => toFile('error', message, meta),
},
})logger takes a Logger from @tanstack/ai. Its four methods are debug, info, warn, and error. Each method gets a message and an optional meta object. The harness puts an error in the meta as { error }. On Node, the default new ConsoleLogger() prints the meta with console.dir.
On a server, call your own logger in each method. This one uses pino:
import pino from 'pino'
import { createHarnessHost } from '@tanstack/ai-harness'
// pino prints the stack of an error only under its `errorKey`, `err` by default.
// The harness puts the error under `error`.
const log = pino({ errorKey: 'error' })
const host = createHarnessHost({
logger: {
debug: (message, meta) => log.debug(meta, message),
info: (message, meta) => log.info(meta, message),
warn: (message, meta) => log.warn(meta, message),
error: (message, meta) => log.error(meta, message),
},
})The harness writes these to your logger:
- Every warning and error of the host and its sessions, also from createHarnessWorker and createHarnessHandler.
- The errors of the model calls of a session: the turn, ctx.model.chat, and the calls of child agents. These calls write no other debug output.
- The warnings of plugins, for example mcp(), codeMode(), and skills(). A plugin writes to your logger with ctx.logger. See Write warnings to the host logger.
If a method of your logger throws, or returns a promise that rejects, that line is lost. The harness goes on.
All options#
Each option is optional. Leave out an option to get its default.
| Option | What it does | Default | Read more |
|---|---|---|---|
| persistence | Where the host saves the state of each session. | In memory. Lost when the process stops. | Durable sessions |
| access | Who can use which session. getUser finds the user of a request. rules decide what that user can do. | No checks. One user. | Access and tenants |
| acceptInput | Checks each input after access, before the log keeps it. It can change the input or refuse it. | Every input is stored as it came. | Check an input on the server |
| credentials | Where the API keys and sign-in tokens of your users are saved. | In memory. | Connect model providers |
| log | How often the host writes streamed text to the log, and when it saves a snapshot. | Text every 100 ms. A snapshot after 1 MB or 5 minutes. | Session log |
| ownership | How servers share the sessions. One server runs a session at a time. | Ownership lasts 30 s and renews every 10 s. Sessions are not pinned to a server. | Durable sessions |
| lifecycle | When the host closes idle sessions, how many stay open, and how much each keeps in memory. | Close after 60 s idle. At most 500 open. | Lifecycle and retention |
| observability | Sends traces to OpenTelemetry. Also calls you when each turn, agent run, and command starts and ends. | No traces. | Deploy a harness |
| logger | Where the harness writes its warnings and errors. Plugins write to it with ctx.logger. | new ConsoleLogger(): the console. | Send the warnings to your own logger |
| presence | Calls you when a client connects to a session, and when it disconnects. | No calls. | Share a thread |
| harnesses | The harnesses whose sessions this host can delete without opening them first. | Only the harnesses that this host opened. | Delete a thread for good |
Some words in this table:
- Log: the record of everything that happened in a session. The host reads it again after a restart.
- Snapshot: a saved copy of the state of a session, so the host does not read the full log to open it.
- Tenant: an organization in your app. With access, the sessions of one tenant stay apart from the sessions of other tenants.
What you have now#
You know which options your app needs. A local app runs with createHarnessHost(). A server adds persistence and access first, and more options only when it needs them.