# 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`): 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)