Threads and state
Threads and state are how a Harness conversation survives beyond a single exchange. A thread is the persistent record of a conversation — its messages and metadata, saved to storage so a user can close the app and resume the same conversation later, or switch between several conversations. State is structured data attached to the conversation — values like model preferences, feature flags, or progress — that agents and your UI read and write as the conversation runs.
The two work together: the thread is the message history, and state is the shared scratchpad alongside it. Both persist across mode switches, model changes, and restarts, so nothing is lost when a user switches from plan mode to build mode or reopens the app the next day.
Thread lifecycle transitions — create, switch, clone, delete — live on the Harness because they coordinate the shared thread lock and emit events. The active thread binding and thread/message reads live on the Session as harness.session.thread.
ThreadsDirect link to Threads
A thread holds one conversation's full message history. The Harness binds the Session to one active thread at a time; messages you send and the agent's replies are appended to that thread and saved to storage. Threads let users resume a past conversation, keep several conversations side by side, or branch one into alternatives.
Creating and selecting threadsDirect link to Creating and selecting threads
On startup, call selectOrCreateThread() to resume the most recent thread or create a new one:
await harness.init()
const thread = await harness.selectOrCreateThread()
Create a thread explicitly with a title:
const thread = await harness.createThread({ title: 'New conversation' })
Switching threadsDirect link to Switching threads
Switch to an existing thread. The harness aborts any in-progress generation, acquires a lock on the new thread, and emits a thread_changed event:
await harness.switchThread({ threadId: 'thread-abc123' })
Listing threadsDirect link to Listing threads
List threads for the current resource. Forked subagent threads are hidden by default:
const threads = await harness.session.thread.list()
// Include all resources
const allThreads = await harness.session.thread.list({ allResources: true })
Cloning threadsDirect link to Cloning threads
Clone a thread to create a branch of the conversation. The harness copies all messages and switches to the clone:
const cloned = await harness.cloneThread({ title: 'Alternative approach' })
Thread lockingDirect link to Thread locking
Pass a threadLock to the Harness constructor to prevent concurrent access from multiple processes. The lock is acquired before any thread operation and released on switch or delete:
const harness = new Harness({
id: 'my-agent',
threadLock: {
acquire: async threadId => {
/* acquire lock or throw */
},
release: async threadId => {
/* release lock */
},
},
})
StateDirect link to State
Where a thread stores the conversation's messages, state stores structured values that describe the conversation but aren't messages — model preferences, feature flags, UI settings, or progress markers. Agents can read and update state during a run, and your UI can react to changes, so state is how the agent and the interface stay in sync on shared facts. You define its shape with a schema, and every update is validated against that schema before it's applied.
Defining a state schemaDirect link to Defining a state schema
Pass a stateSchema (Standard JSON Schema) to validate state and extract defaults:
import { Agent } from '@mastra/core/agent'
import { Harness } from '@mastra/core/harness'
import { z } from 'zod'
const agent = new Agent({
name: 'assistant',
instructions: 'Help the user manage a stateful session.',
model: 'openai/gpt-5.5',
})
const harness = new Harness({
id: 'stateful-agent',
agent,
modes: [{ id: 'default', name: 'Default', metadata: { default: true } }],
stateSchema: z.object({
currentModelId: z.string().optional(),
theme: z.enum(['light', 'dark']).default('dark'),
}),
})
Reading and writing stateDirect link to Reading and writing state
State is owned by the Session as harness.session.state:
// Read the current state snapshot
const state = harness.session.state.get()
// Update state — validates against schema and emits state_changed
await harness.session.state.set({ theme: 'light' })
State changes emit a state_changed event with the new state and the set of changed keys.
Resource IDsDirect link to Resource IDs
Threads are scoped to a resource ID, which groups a conversation's threads by project, user, or workspace. Set it on the Harness constructor; it defaults to the harness id when omitted:
const harness = new Harness({
id: 'my-agent',
resourceId: 'project-xyz',
})
The resource ID is part of a conversation's identity, so you read it from the Session:
const resourceId = harness.session.identity.getResourceId()