TanStack
Harness

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:

ts
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:

A server for many users usually starts like this:

ts
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:

ts
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:

ts
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.

OptionWhat it doesDefaultRead more
persistenceWhere the host saves the state of each session.In memory. Lost when the process stops.Durable sessions
accessWho 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
acceptInputChecks 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
credentialsWhere the API keys and sign-in tokens of your users are saved.In memory.Connect model providers
logHow 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
ownershipHow 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
lifecycleWhen 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
observabilitySends traces to OpenTelemetry. Also calls you when each turn, agent run, and command starts and ends.No traces.Deploy a harness
loggerWhere 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
presenceCalls you when a client connects to a session, and when it disconnects.No calls.Share a thread
harnessesThe 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.