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.
Open the root thread. Each child thread that the harness starts writes to the log of that root:
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' })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.
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)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)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.