> Discover all available pages from the documentation index: https://mastra.ai/llms.txt

# Session class

> **Beta:** The `AgentController` feature is in beta stage and subject to breaking changes in minor versions until it graduates from its beta status.

A `Session` owns all the state tied to a single conversation. The [`AgentController`](https://mastra.ai/reference/agent-controller/agent-controller-class) is the shared host — agents, storage, config, the thread lock, and the event bus — while the `Session` holds everything that is per-conversation: identity, the active thread binding and reads, mode and model selection, run and abort state, the live agent stream, tool suspensions, follow-ups, approvals, permission grants, token usage, and the display-state snapshot.

Access the session through `agentController.session`.

For a conceptual introduction, see the [AgentController overview](https://mastra.ai/docs/agent-controller/overview).

## Usage example

```typescript
// Read per-conversation state through agentController.session
const modeId = agentController.session.mode.get()
const modelId = agentController.session.model.get()
const threadId = agentController.session.thread.getId()
const grants = agentController.session.getGrants()

// Render from the coalesced display-state snapshot
agentController.subscribe(event => {
  if (event.type === 'display_state_changed') {
    render(agentController.session.displayState.get())
  }
})
```

## Properties

The session is organized into sub-objects, each owning one domain of per-conversation state.

**identity** (`SessionIdentity`): Stable session, owner, and resource identity for the conversation. See identity methods below.

**thread** (`SessionThread`): Active thread binding and thread/message reads. See thread methods below.

**mode** (`SessionMode`): Active mode selection. See mode methods below.

**model** (`SessionModel`): Active model selection, including per-mode persistence. See model methods below.

**run** (`SessionRun`): Run and trace identity plus abort state for the in-flight run. See run methods below.

**stream** (`SessionStream`): The live subscription to the agent thread stream. See stream methods below.

**suspensions** (`SessionSuspensions`): Parked interactive tool calls awaiting a resume. See suspensions methods below.

**followUps** (`SessionFollowUps`): Queue of messages submitted while a run is in progress. See follow-up methods below.

**approval** (`SessionApproval`): The pending tool-approval gate. See approval methods below.

**displayState** (`SessionDisplayState`): The canonical AgentControllerDisplayState snapshot a UI renders from. See display-state methods below.

**state** (`SessionState<TState>`): The schema-validated, session-owned AgentController state. See state methods below.

**browser** (`MastraBrowser | undefined`): The browser automation instance for this session. Set at creation via createSession, or from the AgentController config default. Undefined when no browser is configured.

## Methods

### Permissions

Session-scoped grants auto-approve tools without prompting. Grants are ephemeral — they reset when the session restarts and are never persisted.

#### `grantCategory(category)`

Grant a tool category for the current session. Tools in this category are auto-approved.

```typescript
agentController.session.grantCategory('edit')
```

#### `grantTool(toolName)`

Grant a specific tool for the current session.

```typescript
agentController.session.grantTool('mastra_workspace_execute_command')
```

#### `getGrants()`

Return the currently granted categories and tools.

```typescript
const grants = agentController.session.getGrants()
// { categories: string[], tools: string[] }
```

### Tool approvals

#### `respondToToolApproval({ decision, requestContext? })`

Respond to a pending tool approval request, raised by a `tool_approval_required` event. Pass `always_allow_category` to also grant the tool's whole category for the rest of the session.

```typescript
agentController.session.respondToToolApproval({ decision: 'approve' })
agentController.session.respondToToolApproval({ decision: 'decline' })
agentController.session.respondToToolApproval({ decision: 'always_allow_category' })
```

### Run control

#### `getCurrentRunId()`

Return the run ID of the in-flight run, or `null` when idle. Prefers the live stream's active run, falling back to the last stored run ID.

```typescript
const runId = agentController.session.getCurrentRunId()
```

#### `abortRun()`

Abort the in-flight run: aborts the live stream, requests abort on the run, and clears parked tool suspensions.

```typescript
agentController.session.abortRun()
```

### Token usage

#### `getTokenUsage()`

Return a copy of the running token-usage tally for the active thread.

```typescript
const usage = agentController.session.getTokenUsage()
// { promptTokens, completionTokens, totalTokens, ... }
```

## Identity

`session.identity` owns the stable identifiers for the conversation: the resource ID, a session `id`, and an `ownerId`. The `id` and `ownerId` are stable for the life of the session and do not change when the resource ID is switched. They mirror the `id` and `ownerId` fields on `SessionRecord` in storage.

### `session.identity.getId()`

Return the stable session identifier.

```typescript
const sessionId = agentController.session.identity.getId()
```

### `session.identity.getOwnerId()`

Return the stable owner identifier for the session.

```typescript
const ownerId = agentController.session.identity.getOwnerId()
```

### `session.identity.getResourceId()`

Return the current resource ID.

```typescript
const resourceId = agentController.session.identity.getResourceId()
```

### `session.identity.getDefaultResourceId()`

Return the resource ID the session was created with.

```typescript
const defaultResourceId = agentController.session.identity.getDefaultResourceId()
```

To change the resource ID, use [`agentController.setResourceId()`](https://mastra.ai/reference/agent-controller/agent-controller-class), which also tears down the active thread. The session `id` and `ownerId` are not affected by resource switches.

## Thread

`session.thread` owns the active thread binding plus thread and message reads. Thread lifecycle (create, switch, clone, delete, rename) lives on the [`AgentController`](https://mastra.ai/reference/agent-controller/agent-controller-class) because it coordinates the shared thread lock and event bus.

### `session.thread.getId()`

Return the active thread ID, or `null` when no thread is bound.

```typescript
const threadId = agentController.session.thread.getId()
```

### `session.thread.list(options?)`

List threads from storage. By default only threads for the current resource are returned, and transient forked subagent threads are hidden.

```typescript
const threads = await agentController.session.thread.list()
const allThreads = await agentController.session.thread.list({ allResources: true })
const everything = await agentController.session.thread.list({ includeForkedSubagents: true })
```

### `session.thread.getById({ threadId })`

Return a single thread by ID, or `null` if it doesn't exist.

```typescript
const thread = await agentController.session.thread.getById({ threadId: 'thread-abc123' })
```

### `session.thread.listActiveMessages(options?)`

Retrieve messages for the active thread. Returns an empty array when no thread is bound.

```typescript
const messages = await agentController.session.thread.listActiveMessages({ limit: 50 })
```

### `session.thread.listMessages({ threadId, limit? })`

Retrieve messages for a specific thread.

```typescript
const messages = await agentController.session.thread.listMessages({ threadId: 'thread-abc123' })
```

### `session.thread.firstUserMessage({ threadId })`

Retrieve the first user message for a thread, or `null` if none.

```typescript
const firstMsg = await agentController.session.thread.firstUserMessage({
  threadId: 'thread-abc123',
})
```

### `session.thread.firstUserMessages({ threadIds })`

Retrieve the first user message for many threads at once, returned as a map.

```typescript
const firstByThread = await agentController.session.thread.firstUserMessages({
  threadIds: ['thread-a', 'thread-b'],
})
```

### `session.thread.getSetting({ key })` / `setSetting({ key, value })` / `deleteSetting({ key })`

Read, write, and remove per-thread settings stored on the active thread's metadata.

```typescript
await agentController.session.thread.setSetting({ key: 'omThreshold', value: 0.8 })
const value = await agentController.session.thread.getSetting({ key: 'omThreshold' })
await agentController.session.thread.deleteSetting({ key: 'omThreshold' })
```

## Mode

`session.mode` owns the active mode selection.

### `session.mode.get()`

Return the active mode ID.

```typescript
const modeId = agentController.session.mode.get()
```

### `session.mode.resolve()`

Return the full `AgentControllerMode` object for the active mode, resolved against the agentController's configured modes.

```typescript
const mode = agentController.session.mode.resolve()
```

### `session.mode.switch({ modeId })`

Switch to a different mode. Aborts any in-progress generation, saves the current model to the outgoing mode, loads the incoming mode's model, and emits `mode_changed` and `model_changed` events.

```typescript
await agentController.session.mode.switch({ modeId: 'build' })
```

## Model

`session.model` owns the active model selection, including per-mode model memory.

### `session.model.get()`

Return the active model ID.

```typescript
const modelId = agentController.session.model.get()
```

### `session.model.displayName()`

Return a short display name for the active model: the last segment of the model ID (for example, `claude-sonnet-4` from `anthropic/claude-sonnet-4`). Returns `'unknown'` when no model is selected.

```typescript
const name = agentController.session.model.displayName()
```

### `session.model.hasSelection()`

Check whether a model is currently selected.

```typescript
if (agentController.session.model.hasSelection()) {
  // Ready to send messages
}
```

### `session.model.switch({ modelId, scope?, modeId? })`

Switch the active model. When `scope` is `'thread'` (the default), the model ID is persisted as the per-mode model so it's restored when switching back. Reports the selection to the agentController's `modelUseCountTracker` and emits a `model_changed` event.

```typescript
// Set for the current session only
await agentController.session.model.switch({
  modelId: 'anthropic/claude-sonnet-4-6',
  scope: 'global',
})

// Persist to the current thread (default)
await agentController.session.model.switch({ modelId: 'anthropic/claude-sonnet-4-6' })
```

## Observational Memory

The observational-memory model selection, grouped by role under `session.om.observer` and `session.om.reflector`. Both roles expose the same methods. Reads return the value from session state when set, falling back to the agentController's `omConfig` defaults.

### `session.om.observer.modelId()` / `session.om.reflector.modelId()`

Return the role's model ID, or `undefined` when neither session state nor `omConfig` provides one.

```typescript
const observer = agentController.session.om.observer.modelId()
const reflector = agentController.session.om.reflector.modelId()
```

### `session.om.observer.threshold()` / `session.om.reflector.threshold()`

Return the role's threshold in tokens (observation threshold for the observer, reflection threshold for the reflector), or `undefined` when unset.

```typescript
const observationThreshold = agentController.session.om.observer.threshold()
const reflectionThreshold = agentController.session.om.reflector.threshold()
```

### `session.om.observer.switchModel({ modelId })` / `session.om.reflector.switchModel({ modelId })`

Switch the role's model. Persists the setting to thread metadata and emits an `om_model_changed` event.

```typescript
await agentController.session.om.observer.switchModel({
  modelId: 'anthropic/claude-haiku-4-5',
})
await agentController.session.om.reflector.switchModel({
  modelId: 'anthropic/claude-haiku-4-5',
})
```

### `session.om.observer.resolvedModel()` / `session.om.reflector.resolvedModel()`

Resolve the role's model ID to a model instance via the agentController's `resolveModel`, or `undefined` when no model ID is set or no resolver is configured.

```typescript
const observerModel = agentController.session.om.observer.resolvedModel()
```

## Permissions

`session.permissions` owns the persisted tool-approval _policy_ — the per-category and per-tool rules consulted during approval resolution. These are distinct from the in-memory session _grants_ documented under [Methods → Permissions](#permissions); grants reset each session, whereas these rules are persisted in session state.

### `session.permissions.getRules()`

Return the current permission rules, or empty rules (`{ categories: {}, tools: {} }`) when none are set.

```typescript
const rules = agentController.session.permissions.getRules()
// { categories: { execute: 'ask' }, tools: { dangerous_tool: 'deny' } }
```

### `session.permissions.setForCategory({ category, policy })`

Set the approval policy (`'allow' | 'ask' | 'deny'`) for a tool category. Resolves once the change is persisted to session state.

```typescript
await agentController.session.permissions.setForCategory({ category: 'execute', policy: 'ask' })
```

### `session.permissions.setForTool({ toolName, policy })`

Set the approval policy for a specific tool. Per-tool policies take precedence over category policies. Resolves once persisted.

```typescript
await agentController.session.permissions.setForTool({ toolName: 'dangerous_tool', policy: 'deny' })
```

## Subagents

`session.subagents` owns subagent configuration. It currently exposes the subagent model selection under `session.subagents.model`.

### `session.subagents.model.get({ agentType? })`

Return the subagent model ID, preferring the per-`agentType` value when one is given, then the global subagent model, or `null` when neither is set.

```typescript
const modelId = agentController.session.subagents.model.get({ agentType: 'explore' })
```

### `session.subagents.model.set({ modelId, agentType? })`

Set the subagent model ID. Pass an `agentType` to set a per-type override, or omit it to set the global default. Persists to thread settings and emits a `subagent_model_changed` event.

```typescript
// Set the global subagent model
await agentController.session.subagents.model.set({ modelId: 'anthropic/claude-sonnet-4-6' })

// Set a per-type override
await agentController.session.subagents.model.set({
  modelId: 'anthropic/claude-haiku-4-5',
  agentType: 'explore',
})
```

## Run

`session.run` owns run and trace identity plus abort state for the in-flight run.

### `session.run.getRunId()` / `getTraceId()`

Return the stored run ID and trace ID for the current run, or `null` when idle.

```typescript
const runId = agentController.session.run.getRunId()
const traceId = agentController.session.run.getTraceId()
```

### `session.run.isRunning()`

Return whether a run is currently in progress.

```typescript
if (agentController.session.run.isRunning()) {
  // A run is active
}
```

## Stream

`session.stream` owns the live subscription to the agent thread stream and its dedup key.

### `session.stream.activeRunId()`

Return the run ID active on the live stream, or `null` when no stream is open.

```typescript
const runId = agentController.session.stream.activeRunId()
```

### `session.stream.isActive()`

Return whether the stream currently has an active run.

```typescript
if (agentController.session.stream.isActive()) {
  // The current thread's stream is producing output
}
```

## Suspensions

`session.suspensions` owns parked interactive tool calls (such as `ask_user` and `request_access`) awaiting a resume.

### `session.suspensions.hasPending()`

Return whether any tool is currently suspended.

```typescript
if (agentController.session.suspensions.hasPending()) {
  // At least one interactive tool is waiting for a response
}
```

### `session.suspensions.has({ toolCallId })`

Return whether a specific tool call is suspended.

```typescript
const waiting = agentController.session.suspensions.has({ toolCallId: event.toolCallId })
```

Resume a suspended tool with [`agentController.respondToToolSuspension()`](https://mastra.ai/reference/agent-controller/agent-controller-class).

## Follow-ups

`session.followUps` owns the FIFO queue of messages submitted while a run is in progress.

### `session.followUps.count()`

Return the number of queued follow-ups.

```typescript
const queued = agentController.session.followUps.count()
```

### `session.followUps.isEmpty()`

Return whether the follow-up queue is empty.

```typescript
if (!agentController.session.followUps.isEmpty()) {
  // Messages are waiting to be processed
}
```

## Approval

`session.approval` owns the pending tool-approval gate.

### `session.approval.isArmed()`

Return whether a tool is currently awaiting an approval decision.

```typescript
if (agentController.session.approval.isArmed()) {
  // Show the approval prompt
}
```

Respond with [`session.respondToToolApproval()`](#respondtotoolapproval-decision-requestcontext-).

## Display state

`session.displayState` owns the canonical `AgentControllerDisplayState` snapshot a UI renders from, and the reducer that keeps it in sync with every agentController event.

### `session.displayState.get()`

Return the current `AgentControllerDisplayState` snapshot for UI rendering.

```typescript
const displayState = agentController.session.displayState.get()
```

### `session.displayState.restoreTasks(tasks)`

Restore the task portion of the snapshot after a UI replays persisted task tool history. This is a pure update of the snapshot and does not emit an event, so re-render explicitly after calling it.

```typescript
agentController.session.displayState.restoreTasks(replayedTasks)
```

After every event the agentController emits `display_state_changed`, so high-frequency events such as `message_update`, `tool_update`, and `tool_input_delta` are coalesced into the next snapshot. Subscribe with [`agentController.subscribe()`](https://mastra.ai/reference/agent-controller/agent-controller-class) and read the latest snapshot from `session.displayState.get()`.

## State

`session.state` owns the schema-validated AgentController state for the conversation. It holds the current snapshot, validates updates against the `stateSchema` passed to the AgentController, serializes concurrent writes, and emits a `state_changed` event on every change.

### `session.state.get()`

Return a readonly copy of the current state snapshot.

```typescript
const state = agentController.session.state.get()
```

### `session.state.set(updates)`

Merge a partial update into the state. Updates are queued so concurrent calls apply in order, validated against the schema, and emit `state_changed` with the changed keys.

```typescript
await agentController.session.state.set({ yolo: true })
```

### `session.state.update(updater)`

Run an updater against the current snapshot and apply its result atomically within the write queue. Use this for read-modify-write changes that must see the latest state. The updater returns `updates` to merge, optional `events` to emit, and a `result` value that `update()` resolves to.

```typescript
const added = await agentController.session.state.update(current => ({
  updates: { count: (current.count ?? 0) + 1 },
  result: (current.count ?? 0) + 1,
}))
```

## Scoping tags

Sessions carry scoping tags (e.g. `{ projectPath }`) seeded at creation and stamped onto every thread the session creates. Thread listings can be filtered back to the session's scope using these tags.

### `getTags()`

Return a copy of the session's scoping tags. Empty object when the session is unscoped.

```typescript
const tags = agentController.session.getTags()
// { projectPath: '/my/project' }
```

## Related

- [AgentController class](https://mastra.ai/reference/agent-controller/agent-controller-class)
- [AgentController overview](https://mastra.ai/docs/agent-controller/overview)
- [Threads and state](https://mastra.ai/docs/agent-controller/threads-and-state)
- [Tool approvals](https://mastra.ai/docs/agent-controller/tool-approvals)