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. |
Give every server the same log store. Then any server can take a request for any thread:
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)The build imports your entry in a child process to read the harness name, agents, and plugins. Importing a module runs its code.
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.
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.
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.
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:
Without observability, the host writes no spans and does no work for them.
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.
Check these before you run many servers or long threads.
Across servers and restarts:
Users and plugins: