Session
The AgentController feature is in beta stage and subject to breaking changes in minor versions until it graduates from its beta status.
A Session is the isolated runtime for one resource and optional scope. It owns its event bus, thread binding, state, mode and model selections, run control, approvals, suspensions, follow-ups, and display state. The AgentController supplies shared agents, configuration, storage, workspaces, and services.
Create sessions through controller.createSession(). Direct construction and controller wiring methods aren't application APIs.
For a conceptual introduction, see the AgentController overview.
Usage exampleDirect link to Usage example
The following example uses the supported controller-to-session flow.
await controller.init()
const session = await controller.createSession({ resourceId: 'project-42' })
const unsubscribe = session.subscribe(event => {
if (event.type === 'display_state_changed') {
render(event.displayState)
}
})
await session.sendMessage({ content: 'Review the current project.' })
unsubscribe()
PropertiesDirect link to Properties
The session is organized into sub-objects, each owning one domain of per-conversation state.
identity:
thread:
mode:
model:
om:
permissions:
subagents:
run:
stream:
suspensions:
followUps:
approval:
displayState:
state:
browser:
MethodsDirect link to Methods
Identity and eventsDirect link to Identity and events
getTags()Direct link to gettags
Return a copy of the tags supplied when the session was created.
const tags = session.getTags()
Returns: Record<string, string>
subscribe(listener)Direct link to subscribelistener
Subscribe to this session's isolated event bus. The method returns an unsubscribe function.
const unsubscribe = session.subscribe(event => {
console.log(event.type)
})
unsubscribe()
Returns: () => void
Messages and run controlDirect link to Messages and run control
sendMessage({ content, files?, requestContext? })Direct link to sendmessage-content-files-requestcontext-
Send a user message. The session creates a thread first when no thread is active.
await session.sendMessage({
content: 'Summarize this file.',
files: [{ data: fileContents, mediaType: 'text/plain', filename: 'notes.txt' }],
})
steer({ content, requestContext? })Direct link to steer-content-requestcontext-
Queue steering content into an active run.
await session.steer({ content: 'Focus on the failing tests.' })
followUp({ content, requestContext? })Direct link to followup-content-requestcontext-
Queue a follow-up while a run is active, or send it immediately while idle.
await session.followUp({ content: 'Then propose a fix.' })
getCurrentRunId()Direct link to getcurrentrunid
Return the active stream run identifier, the tracked run identifier, or null while idle.
const runId = session.getCurrentRunId()
Returns: string | null
abort()Direct link to abort
Abort the active run and clear pending suspension display state.
session.abort()
WorkspaceDirect link to Workspace
getWorkspace()Direct link to getworkspace
Return the workspace resolved for this session. This preserves session-level overrides and workspaces selected from the session scope.
const workspace = session.getWorkspace()
const skill = await workspace.skills?.get('code-review')
Returns: Workspace
Session grantsDirect link to Session grants
Session-scoped grants auto-approve tools without prompting. Grants are ephemeral: they reset when the session restarts and are never persisted.
grantCategory(category)Direct link to grantcategorycategory
Grant a tool category for the current session. Tools in this category are auto-approved.
session.grantCategory('edit')
grantTool(toolName)Direct link to granttooltoolname
Grant a specific tool for the current session.
session.grantTool('mastra_workspace_execute_command')
getGrants()Direct link to getgrants
Return the currently granted categories and tools.
const grants = session.getGrants()
// { categories: string[], tools: string[] }
hasCategoryGrant(category)Direct link to hascategorygrantcategory
Return whether a category has an in-memory session grant.
const allowed = session.hasCategoryGrant('edit')
Returns: boolean
hasToolGrant(toolName)Direct link to hastoolgranttoolname
Return whether a tool has an in-memory session grant.
const allowed = session.hasToolGrant('write_file')
Returns: boolean
Tool approvalsDirect link to Tool approvals
resolveToolApproval(toolName)Direct link to resolvetoolapprovaltoolname
Return the effective policy after applying explicit tool rules, session grants, and category rules.
const policy = session.resolveToolApproval('execute_command')
Returns: PermissionPolicy
respondToToolApproval({ decision, toolCallId?, requestContext?, declineContext? })Direct link to respondtotoolapproval-decision-toolcallid-requestcontext-declinecontext-
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.
session.respondToToolApproval({ decision: 'approve' })
session.respondToToolApproval({ decision: 'decline' })
session.respondToToolApproval({ decision: 'always_allow_category' })
respondToToolSuspension({ resumeData, toolCallId?, requestContext? })Direct link to respondtotoolsuspension-resumedata-toolcallid-requestcontext-
Resume a suspended tool with application-provided data. Supply toolCallId when several tool calls are suspended.
await session.respondToToolSuspension({
toolCallId: event.toolCallId,
resumeData: ['src'],
})
For submit_plan, pass { action: 'approved' } or { action: 'rejected', feedback }. Approval can switch to the mode configured by transitionsTo before the tool resumes.
Token usageDirect link to Token usage
getTokenUsage()Direct link to gettokenusage
Return a copy of the running token-usage tally for the active thread.
const usage = session.getTokenUsage()
// { promptTokens, completionTokens, totalTokens, ... }
IdentityDirect link to 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 don't change when the resource ID is switched. They mirror the id and ownerId fields on SessionRecord in storage.
session.identity.getId()Direct link to sessionidentitygetid
Return the stable session identifier.
const sessionId = session.identity.getId()
session.identity.getOwnerId()Direct link to sessionidentitygetownerid
Return the stable owner identifier for the session.
const ownerId = session.identity.getOwnerId()
session.identity.getResourceId()Direct link to sessionidentitygetresourceid
Return the current resource ID.
const resourceId = session.identity.getResourceId()
session.identity.getDefaultResourceId()Direct link to sessionidentitygetdefaultresourceid
Return the resource ID the session was created with.
const defaultResourceId = session.identity.getDefaultResourceId()
To change the resource ID, use controller.setResourceId(), which also clears the active thread. The session id and ownerId aren't affected by resource switches.
ThreadDirect link to Thread
session.thread owns the active thread binding and resource-scoped thread operations. Stored threads and messages can survive controller recreation when storage is configured. The live session and its event bus don't.
session.thread.create({ title?, id? })Direct link to sessionthreadcreate-title-id-
Create a thread, bind the session to it, and open its event stream.
const thread = await session.thread.create({
id: 'thread-7',
title: 'Investigate login failure',
})
Returns: Promise<AgentControllerThread>
session.thread.rename({ title })Direct link to sessionthreadrename-title-
Rename the active stored thread.
await session.thread.rename({ title: 'Fix login failure' })
session.thread.clone({ sourceThreadId?, title?, resourceId? })Direct link to sessionthreadclone-sourcethreadid-title-resourceid-
Clone an owned thread and its messages, then bind the session to the clone.
const clone = await session.thread.clone({
sourceThreadId: 'thread-7',
title: 'Alternative approach',
})
Returns: Promise<AgentControllerThread>
session.thread.switch({ threadId, emitEvent? })Direct link to sessionthreadswitch-threadid-emitevent-
Switch to an owned stored thread and hydrate its mode, model, and observational memory settings.
await session.thread.switch({ threadId: 'thread-8' })
session.thread.delete({ threadId })Direct link to sessionthreaddelete-threadid-
Delete an owned thread. Deleting the active thread also clears the current binding.
await session.thread.delete({ threadId: 'thread-8' })
session.thread.getId()Direct link to sessionthreadgetid
Return the active thread ID, or null when no thread is bound.
const threadId = session.thread.getId()
session.thread.list(options?)Direct link to sessionthreadlistoptions
List threads from storage. By default only threads for the current resource are returned, and transient forked subagent threads are hidden.
const threads = await session.thread.list()
const allThreads = await session.thread.list({ allResources: true })
const everything = await session.thread.list({ includeForkedSubagents: true })
session.thread.getById({ threadId })Direct link to sessionthreadgetbyid-threadid-
Return a single thread by ID, or null if it doesn't exist.
const thread = await session.thread.getById({ threadId: 'thread-abc123' })
session.thread.listActiveMessages(options?)Direct link to sessionthreadlistactivemessagesoptions
Retrieve messages for the active thread. Returns an empty array when no thread is bound.
const messages = await session.thread.listActiveMessages({ limit: 50 })
session.thread.listMessages({ threadId, limit? })Direct link to sessionthreadlistmessages-threadid-limit-
Retrieve messages for a specific thread.
const messages = await session.thread.listMessages({ threadId: 'thread-abc123' })
The message-reading methods listActiveMessages, listMessages, and firstUserMessage return MastraDBMessage objects, while firstUserMessages returns a Map<string, MastraDBMessage> keyed by thread ID. Each message has a role, an id, a createdAt, and a content object with content.format and a content.parts array. Read text, reasoning, tool calls, and attachments from content.parts. Signals such as system reminders and notifications are returned as separate messages with role: 'signal'.
session.thread.firstUserMessage({ threadId })Direct link to sessionthreadfirstusermessage-threadid-
Retrieve the first user message for a thread, or null if none.
const firstMsg = await session.thread.firstUserMessage({
threadId: 'thread-abc123',
})
session.thread.firstUserMessages({ threadIds })Direct link to sessionthreadfirstusermessages-threadids-
Retrieve the first user message for many threads at once, returned as a map.
const firstByThread = await session.thread.firstUserMessages({
threadIds: ['thread-a', 'thread-b'],
})
session.thread.getSetting({ key })Direct link to sessionthreadgetsetting-key-
Read a setting from the active thread metadata.
const value = await session.thread.getSetting({ key: 'omThreshold' })
session.thread.setSetting({ key, value })Direct link to sessionthreadsetsetting-key-value-
Write a setting to the active thread metadata.
await session.thread.setSetting({ key: 'omThreshold', value: 0.8 })
session.thread.deleteSetting({ key })Direct link to sessionthreaddeletesetting-key-
Remove a setting from the active thread metadata.
await session.thread.deleteSetting({ key: 'omThreshold' })
ModeDirect link to Mode
session.mode owns the active mode selection.
session.mode.get()Direct link to sessionmodeget
Return the active mode ID.
const modeId = session.mode.get()
session.mode.resolve()Direct link to sessionmoderesolve
Return the full AgentControllerMode object for the active mode, resolved against the controller's configured modes.
const mode = session.mode.resolve()
session.mode.switch({ modeId })Direct link to sessionmodeswitch-modeid-
Switch to another mode. The session saves the outgoing mode's model before persisting the new mode on the active thread. It then restores the incoming mode's selected or default model. The session emits mode_changed immediately and model_changed after model resolution.
await session.mode.switch({ modeId: 'build' })
ModelDirect link to Model
session.model owns the active model selection, including per-mode model memory.
session.model.get()Direct link to sessionmodelget
Return the active model ID.
const modelId = session.model.get()
session.model.displayName()Direct link to sessionmodeldisplayname
Return the last segment of the active model ID as a short display name. Returns 'unknown' when no model is selected.
const name = session.model.displayName()
session.model.hasSelection()Direct link to sessionmodelhasselection
Check whether a model is currently selected.
if (session.model.hasSelection()) {
// Ready to send messages
}
session.model.switch({ modelId, scope?, modeId? })Direct link to sessionmodelswitch-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 controller's modelUseCountTracker and emits a model_changed event.
// Set for the current session only
await session.model.switch({
modelId: 'anthropic/claude-sonnet-4-6',
scope: 'global',
})
// Persist to the current thread (default)
await session.model.switch({ modelId: 'anthropic/claude-sonnet-4-6' })
Observational MemoryDirect link to 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 controller's omConfig defaults.
session.om.observer.modelId() / session.om.reflector.modelId()Direct link to sessionomobservermodelid--sessionomreflectormodelid
Return the role's model ID, or undefined when neither session state nor omConfig provides one.
const observer = session.om.observer.modelId()
const reflector = session.om.reflector.modelId()
session.om.observer.threshold() / session.om.reflector.threshold()Direct link to sessionomobserverthreshold--sessionomreflectorthreshold
Return the role's threshold in tokens (observation threshold for the observer, reflection threshold for the reflector), or undefined when unset.
const observationThreshold = session.om.observer.threshold()
const reflectionThreshold = session.om.reflector.threshold()
session.om.observer.switchModel({ modelId }) / session.om.reflector.switchModel({ modelId })Direct link to sessionomobserverswitchmodel-modelid---sessionomreflectorswitchmodel-modelid-
Switch the role's model. Persists the setting to thread metadata and emits an om_model_changed event.
await session.om.observer.switchModel({
modelId: 'anthropic/claude-haiku-4-5',
})
await session.om.reflector.switchModel({
modelId: 'anthropic/claude-haiku-4-5',
})
session.om.observer.resolvedModel() / session.om.reflector.resolvedModel()Direct link to sessionomobserverresolvedmodel--sessionomreflectorresolvedmodel
Resolve the role's model ID to a model instance via the configured model gateways, or undefined when no model ID is set or no resolver is configured.
const observerModel = session.om.observer.resolvedModel()
const reflectorModel = session.om.reflector.resolvedModel()
PermissionsDirect link to Permissions
session.permissions owns the tool-approval policy represented in session.state: the per-category and per-tool rules consulted during approval resolution. These are distinct from the in-memory grants documented under Session grants. Grants reset with the live session. Permission rules aren't durable unless the host restores the corresponding session state.
session.permissions.getRules()Direct link to sessionpermissionsgetrules
Return the current permission rules, or empty rules ({ categories: {}, tools: {} }) when none are set.
const rules = session.permissions.getRules()
// { categories: { execute: 'ask' }, tools: { dangerous_tool: 'deny' } }
session.permissions.setForCategory({ category, policy })Direct link to sessionpermissionssetforcategory-category-policy-
Set the approval policy ('allow' | 'ask' | 'deny') for a tool category. Resolves once the change is persisted to session state.
await session.permissions.setForCategory({ category: 'execute', policy: 'ask' })
session.permissions.setForTool({ toolName, policy })Direct link to sessionpermissionssetfortool-toolname-policy-
Set the approval policy for a specific tool. Per-tool policies take precedence over category policies. Resolves once persisted.
await session.permissions.setForTool({ toolName: 'dangerous_tool', policy: 'deny' })
SubagentsDirect link to Subagents
session.subagents owns subagent configuration. It currently exposes the subagent model selection under session.subagents.model.
session.subagents.model.get({ agentType? })Direct link to sessionsubagentsmodelget-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.
const modelId = session.subagents.model.get({ agentType: 'explore' })
session.subagents.model.set({ modelId, agentType? })Direct link to sessionsubagentsmodelset-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.
// Set the global subagent model
await session.subagents.model.set({ modelId: 'anthropic/claude-sonnet-4-6' })
// Set a per-type override
await session.subagents.model.set({
modelId: 'anthropic/claude-haiku-4-5',
agentType: 'explore',
})
RunDirect link to Run
session.run owns run and trace identity plus abort state for the in-flight run.
session.run.getRunId() / getTraceId()Direct link to sessionrungetrunid--gettraceid
Return the stored run ID and trace ID for the current run, or null when idle.
const runId = session.run.getRunId()
const traceId = session.run.getTraceId()
session.run.isRunning()Direct link to sessionrunisrunning
Return whether a run is currently in progress.
if (session.run.isRunning()) {
// A run is active
}
StreamDirect link to Stream
session.stream owns the live subscription to the agent thread stream and its dedup key.
session.stream.activeRunId()Direct link to sessionstreamactiverunid
Return the run ID active on the live stream, or null when no stream is open.
const runId = session.stream.activeRunId()
session.stream.isActive()Direct link to sessionstreamisactive
Return whether the stream currently has an active run.
if (session.stream.isActive()) {
// The current thread's stream is producing output
}
SuspensionsDirect link to Suspensions
session.suspensions owns parked interactive tool calls (such as ask_user and request_access) awaiting a resume.
session.suspensions.hasPending()Direct link to sessionsuspensionshaspending
Return whether any tool is currently suspended.
if (session.suspensions.hasPending()) {
// At least one interactive tool is waiting for a response
}
session.suspensions.has({ toolCallId })Direct link to sessionsuspensionshas-toolcallid-
Return whether a specific tool call is suspended.
const waiting = session.suspensions.has({ toolCallId: event.toolCallId })
Resume a suspended tool with session.respondToToolSuspension().
Follow-upsDirect link to Follow-ups
session.followUps owns the FIFO queue of messages submitted while a run is in progress.
session.followUps.count()Direct link to sessionfollowupscount
Return the number of queued follow-ups.
const queued = session.followUps.count()
session.followUps.isEmpty()Direct link to sessionfollowupsisempty
Return whether the follow-up queue is empty.
if (!session.followUps.isEmpty()) {
// Messages are waiting to be processed
}
ApprovalDirect link to Approval
session.approval owns the pending tool-approval gate.
session.approval.isArmed()Direct link to sessionapprovalisarmed
Return whether a tool is currently awaiting an approval decision.
if (session.approval.isArmed()) {
// Show the approval prompt
}
Respond with session.respondToToolApproval().
Display stateDirect link to Display state
session.displayState owns the canonical AgentControllerDisplayState snapshot a UI renders from, and the reducer that keeps it in sync with every session event.
session.displayState.get()Direct link to sessiondisplaystateget
Return the current AgentControllerDisplayState snapshot for UI rendering.
const displayState = session.displayState.get()
session.displayState.restoreTasks(tasks)Direct link to sessiondisplaystaterestoretaskstasks
Restore the task portion of the snapshot after a UI replays persisted task tool history. This is a pure update of the snapshot and doesn't emit an event, so re-render explicitly after calling it.
session.displayState.restoreTasks(replayedTasks)
After every event, the session emits display_state_changed with the latest snapshot. Subscribe with session.subscribe() or read the current value from session.displayState.get().
StateDirect link to State
session.state owns the schema-validated AgentController state for the conversation. It holds the current snapshot and validates updates against the stateSchema passed to the AgentController. Updates are serialized, and every change emits a state_changed event.
session.state.get()Direct link to sessionstateget
Return a readonly copy of the current state snapshot.
const state = session.state.get()
session.state.set(updates)Direct link to sessionstatesetupdates
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.
await session.state.set({ yolo: true })
session.state.update(updater)Direct link to sessionstateupdateupdater
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.
const added = await session.state.update(current => ({
updates: { count: (current.count ?? 0) + 1 },
result: (current.count ?? 0) + 1,
}))
Persistence boundariesDirect link to Persistence boundaries
A Session is a live runtime object. Its event bus, arbitrary session.state, permission rules, permission grants, pending approvals, suspensions, follow-ups, run state, and stream state don't automatically survive controller or process recreation. The host must restore any of this state when recreating a session.
With configured storage, threads, messages, and token usage persist. Thread settings restore mode and model choices. They can also restore observational memory settings and subagent model selections, including per-agent-type overrides. A chat channel can map back to stored threads, but channel-to-session and auto-approval state held by AgentControllerChannels remains in memory.