@mastra/core
Version:
144 lines (100 loc) • 5.96 kB
Markdown
> Discover all available pages from the documentation index: https://mastra.ai/llms.txt
# Threads and state
Threads and state are how an AgentController 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 AgentController because they coordinate the shared thread lock and emit events. The active thread binding and thread/message _reads_ live on the [`Session`](https://mastra.ai/docs/agent-controller/session) as `agentController.session.thread`.
## Threads
A thread holds one conversation's full message history. The AgentController 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 threads
On startup, call `selectOrCreateThread()` to resume the most recent thread or create a new one:
```typescript
await agentController.init()
const thread = await agentController.selectOrCreateThread()
```
Create a thread explicitly with a title:
```typescript
const thread = await agentController.createThread({ title: 'New conversation' })
```
### Switching threads
Switch to an existing thread. The agentController aborts any in-progress generation, acquires a lock on the new thread, and emits a `thread_changed` event:
```typescript
await agentController.switchThread({ threadId: 'thread-abc123' })
```
### Listing threads
List threads for the current resource. Forked subagent threads are hidden by default:
```typescript
const threads = await agentController.session.thread.list()
// Include all resources
const allThreads = await agentController.session.thread.list({ allResources: true })
```
### Cloning threads
Clone a thread to create a branch of the conversation. The agentController copies all messages and switches to the clone:
```typescript
const cloned = await agentController.cloneThread({ title: 'Alternative approach' })
```
### Thread locking
Pass a `threadLock` to the AgentController constructor to prevent concurrent access from multiple processes. The lock is acquired before any thread operation and released on switch or delete:
```typescript
const agentController = new AgentController({
id: 'my-agent',
threadLock: {
acquire: async threadId => {
/* acquire lock or throw */
},
release: async threadId => {
/* release lock */
},
},
})
```
## 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 schema
Pass a `stateSchema` (Standard JSON Schema) to validate state and extract defaults:
```typescript
import { Agent } from '@mastra/core/agent'
import { AgentController } from '@mastra/core/agent-controller'
import { z } from 'zod'
const agent = new Agent({
id: 'assistant',
name: 'assistant',
instructions: 'Help the user manage a stateful session.',
model: 'openai/gpt-5.5',
})
const agentController = new AgentController({
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 state
State is owned by the [`Session`](https://mastra.ai/docs/agent-controller/session) as `agentController.session.state`:
```typescript
// Read the current state snapshot
const state = agentController.session.state.get()
// Update state — validates against schema and emits state_changed
await agentController.session.state.set({ theme: 'light' })
```
State changes emit a `state_changed` event with the new state and the set of changed keys.
## Resource IDs
Threads are scoped to a resource ID, which groups a conversation's threads by project, user, or workspace. Set it on the AgentController constructor; it defaults to the agentController `id` when omitted:
```typescript
const agentController = new AgentController({
id: 'my-agent',
resourceId: 'project-xyz',
})
```
The resource ID is part of a conversation's identity, so you read it from the Session:
```typescript
const resourceId = agentController.session.identity.getResourceId()
```
The session also has a stable `id` and `ownerId` (read with `session.identity.getId()` and `session.identity.getOwnerId()`). Unlike the resource ID, these don't change when you switch resources — see [Session identity](https://mastra.ai/docs/agent-controller/session) for details.
## Related
- [AgentController overview](https://mastra.ai/docs/agent-controller/overview)
- [Session](https://mastra.ai/docs/agent-controller/session)
- [Modes](https://mastra.ai/docs/agent-controller/modes)
- [API reference](https://mastra.ai/reference/agent-controller/agent-controller-class)