- 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
One log for a session tree
Your framework runs one agent with a main conversation and child tasks. If each conversation has its own log, two things go wrong. A record for the child and a record for the parent can land one without the other after a crash. And no one place holds the state of the whole agent. A harness keeps the whole tree in one log, so the threads share its order, its atomic appends, and one state fold.
One log per session tree#
Open the root thread. Each child thread that the harness starts writes to the log of that root:
- An agent run and its follow-ups (<threadId>:<name>:<input id>).
- A subagent run (subagent:<runId>).
- A session of another harness that runs as a child with harnessAgent (<root thread id>/agent/<run id>).
import { createHarnessHost, defineHarness } from '@tanstack/ai-harness'
import { memoryPersistence } from '@tanstack/ai-persistence'
import { openaiText } from '@tanstack/ai-openai'
const persistence = memoryPersistence()
const assistant = defineHarness({
name: 'acme/assistant',
adapter: openaiText('gpt-6.1-sol'),
})
const host = createHarnessHost({ persistence })
const root = await host.open(assistant, { threadId: 'agent-1' })- The log has the id of the root thread, in its tenant: logIdOf({ tenantId, logId: 'agent-1' }).
- Each thread folds only its own transcript, inputs, and events from the log. A child never sees the messages of the root.
- A child thread does not open. Open its root, and read the child with session.agentTranscript({ subagentRunId }). host.open throws for the thread id of a harnessAgent child.
- host.observe also reads a child thread by its own thread id. It finds the root through the session index, so it needs stores.sessions. memoryPersistence() has it.
Write to several threads in one append#
session.append adds records to the root thread. A record with thread: '<other threadId>' belongs to that thread of the same log. The whole append is all or nothing:
await root.appendRecords({
records: [
{ type: 'app.child_created', thread: 'task-1', task: 'research' },
{ type: 'app.child_linked', child: 'task-1' },
],
})Here the child record and the link in the root land together, or not at all.
- thread must be a string. Any other value makes appendRecords throw.
- A record without thread belongs to the root thread.
Keep state for the whole tree#
Give the harness a durability.logState option. It folds every host record of the log, from the root and from each child thread, in log order. Then session.logState() reads the result:
import type { ReduceOptions } from '@tanstack/ai-harness'
interface Children {
tasks: Array<string>
}
const reduce: ReduceOptions<Children> = {
initial: { tasks: [] },
version: 'v1',
record: ({ state, record }) =>
record.type === 'app.child_created' && typeof record.thread === 'string'
? { tasks: [...state.tasks, record.thread] }
: state,
}
const tracker = defineHarness({
name: 'acme/tracker',
adapter: openaiText('gpt-6.1-sol'),
durability: { logState: reduce },
})
const tracked = await host.open(tracker, { threadId: 'agent-2' })
await tracked.appendRecords({
records: [{ type: 'app.child_created', thread: 'task-2', task: 'research' }],
})
console.log((await tracked.logState())?.tasks)- record must be pure. It returns the next state, or the same state for no change.
- The type of the state flows from initial and record. Here logState() resolves to Children | undefined. A harness without logState gets undefined.
- The fold comes from the log, so a session on another machine gets the same state.
- Change version when you change record. A log snapshot with another version is not used, so the host folds the log again from the first record.
- durability.addRecordsToModelContext also works with logState. It folds a host record into the messages of its own thread only.
- Every harness that opens the same log must pass the same addRecordsToModelContext and logState objects. When a harness with other ones opens a log that is open, host.open throws, and so do the reads of host.observe.
A read route that opens no session gets the same state from a view:
const view = await host.observe(tracker, { threadId: 'agent-2' })
console.log((await view.logState())?.tasks)One writer per log#
Only one host writes a log at a time. When another host takes the log, the sessions of that log on this host stay open and read on. Their next write takes the log back, when no other host owns it. See One host drives a thread.
One root thread has one open session per host. A second harness on the same thread throws.
To read a root thread outside a session, for example for reconstructChat, use logMessageStore:
import { logMessageStore } from '@tanstack/ai-harness'
const messages = logMessageStore({ store: persistence.stores.log })
console.log(await messages.loadThread('agent-1'))Each thread id that you pass is the root of a tree. To read a child thread, use host.observe.
What you have now#
- One durable log for a root thread and all of its child threads, each with its own transcript.
- Appends that reach several threads and land all together or not at all.
- One state for the whole tree, from durability.logState, that you read with session.logState() on any machine.