@mastra/core
Version:
1,068 lines (1,067 loc) • 61 kB
TypeScript
import type { Agent } from '../agent/index.js';
import type { AgentSignalAttributes, AgentSignalContents, AgentSignalInput } from '../agent/signals.js';
import type { AgentThreadSubscription, MastraBrowser, SendAgentNotificationSignalOptions, SendAgentNotificationSignalResult, ToolsetsInput } from '../agent/types.js';
import type { MastraModelGatewayInterface } from '../llm/model/gateways/index.js';
import type { MastraModelConfig } from '../llm/model/shared.types.js';
import type { SendNotificationSignalInput } from '../notifications/index.js';
import type { TracingContext, TracingOptions } from '../observability/index.js';
import type { RequestContext } from '../request-context/index.js';
import type { PublicSchema } from '../schema/index.js';
import { Workspace } from '../workspace/index.js';
import { SessionRunEngine } from './session-run-engine.js';
import type { TaskItemSnapshot } from './tools.js';
import type { AgentControllerDisplayState, AgentControllerEvent, AgentControllerEventListener, AgentControllerMessage, AgentControllerMode, AgentControllerOMConfig, AgentControllerRequestState, AgentControllerThread, ModelUseCountTracker, PermissionPolicy, PermissionRules, TokenUsage, ToolCategory } from './types.js';
/**
* Minimal persistence surface the Session uses to read and write per-thread
* settings (mode id, per-mode model id, …). The AgentController backs this with thread
* metadata; when no storage is configured it is absent and the Session keeps
* its state purely in memory.
*/
export interface ThreadSettingsStore {
/** Read a setting for the active thread, or undefined when unset/unavailable. */
get(key: string): Promise<unknown>;
/** Persist a setting for the active thread (no-op when storage is unavailable). */
set(key: string, value: unknown): Promise<void>;
}
/** Options for {@link Session.sendNotificationSignal}. */
export type SessionSendNotificationSignalOptions = {
ifActive?: SendAgentNotificationSignalOptions['ifActive'];
ifIdle?: SendAgentNotificationSignalOptions['ifIdle'];
tracingContext?: TracingContext;
tracingOptions?: TracingOptions;
requestContext?: RequestContext;
};
/**
* Owns the session's identity: the memory `resourceId` and the active
* `threadId` this session reads and writes under. Together they form the memory
* binding (`{ thread, resource }`) every run uses. In a multi-user host one
* AgentController serves many sessions, so this identity — "whose session is this, and
* which thread is it on" — belongs to the Session, not the AgentController.
*
* `defaultResourceId` is the resourceId the session started with; switching to a
* different resource (e.g. impersonation, or browsing another user's threads)
* updates the current resourceId while the default is retained so the session
* can return to its own identity.
*
* `id` is the stable identifier for this session (mirrors `SessionRecord.id` in
* storage) and `ownerId` is the owner of this session (mirrors
* `SessionRecord.ownerId`). Both are stable for the life of the session and do
* not change when the resourceId is switched.
*
* The active thread the session is bound to lives on {@link SessionThread}, not
* here — identity is the stable "who", the thread is the navigational "where".
*/
export declare class SessionIdentity {
#private;
constructor({ resourceId, id, ownerId }: {
resourceId: string;
id: string;
ownerId: string;
});
/** The resourceId the session currently reads/writes under. */
getResourceId(): string;
/** The resourceId the session started with. */
getDefaultResourceId(): string;
/** The stable session identifier for this session. */
getId(): string;
/** The stable owner identifier for this session. */
getOwnerId(): string;
/** Point the session at a different resourceId (the default is unchanged). */
setResourceId({ resourceId }: {
resourceId: string;
}): void;
}
/**
* The shared-host storage surface the Session's thread domain leverages to read
* and write threads. The AgentController backs this with its memory storage (mapping raw
* storage rows to {@link AgentControllerThread}/{@link AgentControllerMessage}); when no storage
* is configured the handle is absent and the data methods degrade gracefully
* (empty lists, undefined settings, no-op writes).
*
* This is a gateway to shared infrastructure — not a callback into AgentController
* orchestration. The Session owns the thread-domain logic; the host owns the DB.
*/
export interface ThreadDataStore {
/** List threads for a resource (or all resources), already mapped + filtered of forked subagents unless asked. */
listThreads(input: {
resourceId?: string;
includeForkedSubagents?: boolean;
metadata?: Record<string, unknown>;
}): Promise<AgentControllerThread[]>;
/** Fetch a single thread by id, or null when it doesn't exist. */
getById(input: {
threadId: string;
}): Promise<AgentControllerThread | null>;
/** List messages for a thread, newest-`limit` (returned oldest-first) or all. */
listMessages(input: {
threadId: string;
limit?: number;
}): Promise<AgentControllerMessage[]>;
/** The first user message for each given thread id. */
firstUserMessages(input: {
threadIds: string[];
}): Promise<Map<string, AgentControllerMessage>>;
/** Read a value from a thread's metadata. */
getMetadata(input: {
threadId: string;
key: string;
}): Promise<unknown>;
/** Write a value into a thread's metadata. */
setMetadata(input: {
threadId: string;
key: string;
value: unknown;
}): Promise<void>;
/** Delete a value from a thread's metadata. */
deleteMetadata(input: {
threadId: string;
key: string;
}): Promise<void>;
/** Whether the host has thread storage configured. When false, lifecycle persistence is a no-op. */
hasStorage(): boolean;
/** Persist a new or updated thread row. No-op when storage is unavailable. */
saveThread(input: {
thread: AgentControllerThread;
}): Promise<void>;
/** Delete a thread row by id. No-op when storage is unavailable. */
deleteThread(input: {
threadId: string;
}): Promise<void>;
/** Clone a thread (and its messages) via the host's memory, returning the new thread. */
cloneThread(input: {
sourceThreadId: string;
resourceId: string;
title?: string;
metadata?: Record<string, unknown>;
}): Promise<AgentControllerThread>;
/** Acquire the host thread lock for a thread id. No-op when no lock is configured. */
acquireLock(threadId: string): Promise<void>;
/** Release the host thread lock for a thread id. No-op when no lock is configured. */
releaseLock(threadId: string): Promise<void>;
/** The host's configured mode ids, used to validate a thread's persisted mode on restore. */
getModeIds(): string[];
}
/**
* The AgentController-owned machinery a Session leverages to drive an agent run. In the
* multi-user host one AgentController serves many sessions; the run loop, run state, and
* thread stream are per-session (they cannot be shared) and so belong on the
* Session. But *how* a run is produced — which agent answers, the config-backed
* run/stream options, the toolset, the request context, the tool-approval
* policy, usage persistence, id generation — is shared infrastructure the
* AgentController owns. The AgentController injects this machinery into each Session it
* constructs (via {@link Session.setMachinery}); the Session calls into it but
* never reaches back into the AgentController or another session.
*
* This is the formalized DI boundary: the Session receives exactly the
* capabilities it is allowed to use, nothing more.
*/
export interface SessionMachinery {
/** Resolve the agent that should answer for the session's current mode/model. */
getAgent(): Agent;
/** Open a fresh subscription to a thread's agent event stream. */
subscribeToThread(input: {
resourceId: string;
threadId: string;
}): Promise<AgentThreadSubscription<any>>;
/** Build the per-call stream options (instructions, memory, toolsets, abort signal, tracing). */
buildStreamOptions(input: {
requestContext?: RequestContext;
tracingContext?: TracingContext;
tracingOptions?: TracingOptions;
}): Promise<Record<string, unknown>>;
/** The run budget every initial stream and resume must carry (maxSteps, provider fallbacks, …). */
buildSharedRunOptions(): Record<string, unknown>;
/** Resolve the toolset (built-in controller tools + user/subagent tools) for a run. */
buildToolsets(requestContext: RequestContext): Promise<ToolsetsInput>;
/** Resolve the effective request context for a run, layering controller defaults. */
buildRequestContext(requestContext?: RequestContext): Promise<RequestContext>;
/** Persist the session's running token usage to thread metadata. */
persistTokenUsage(): Promise<void>;
/** Generate a new id (thread ids, message ids) using the host's id strategy. */
generateId(): string;
/**
* Resolve the mode the session transitions to when a plan is approved: the
* current mode's `transitionsTo`, else the host's default mode. Returns
* `undefined` when the host has no default mode. The mode catalog is AgentController
* config, so this is genuinely host-owned.
*/
resolveTransitionModeId(): string | undefined;
/**
* Persist a system-reminder message to a thread, returning the saved message
* (or `null` when no storage is configured). Pure host-owned persistence
* (storage handle + id strategy).
*/
saveSystemReminder(input: {
threadId: string;
resourceId: string;
message: string;
reminderType: string;
role: 'user' | 'assistant' | 'system';
metadata?: Record<string, unknown>;
}): Promise<AgentControllerMessage | null>;
}
/**
* Owns the session's thread domain: the navigational binding (which thread the
* session is currently on) plus the data reads/queries scoped to it. `null`
* until the session is bound (a thread is created, switched to, or reacquired on
* startup); switching/deleting updates it.
*
* In the multi-user model each session has its own current thread and reads its
* own threads, while the AgentController host shares storage, the thread lock, and the
* event bus. So the binding + data queries are per-session and live here; the
* session leverages the host's storage via an injected {@link ThreadDataStore}.
* Lifecycle *transitions* (create/switch/clone/delete) remain host machinery
* because they drive the shared event bus and rebind the shared agent stream.
*/
export declare class SessionThread {
#private;
constructor(getResourceId: () => string);
/**
* Attach the shared-host storage gateway the thread domain reads/writes
* through and the owning session whose subsystems lifecycle transitions
* orchestrate. The AgentController calls this once during wiring; without a store the
* data methods degrade gracefully.
*/
connect(store: ThreadDataStore | undefined, session: Session): void;
/** The active thread id, or null when the session is not bound to a thread. */
getId(): string | null;
/** Whether the session is currently bound to a thread. */
isSet(): boolean;
/** The active thread id, throwing when the session is not bound to a thread. */
requireId(): string;
/** Bind the session to a thread. */
set({ threadId }: {
threadId: string;
}): void;
/** Clear the session's thread binding. */
clear(): void;
/** Clear the session's thread binding and release its lock when one is held. */
clearAndReleaseLock(): Promise<void>;
/** List this session's threads (its own resource by default, or all resources). */
list(options?: {
allResources?: boolean;
includeForkedSubagents?: boolean;
metadata?: Record<string, unknown>;
}): Promise<AgentControllerThread[]>;
/** Fetch a single thread by id, or null when it doesn't exist / no storage. */
getById({ threadId }: {
threadId: string;
}): Promise<AgentControllerThread | null>;
/** Clone a detected cross-resource project thread into this session's resource. */
cloneToCurrentResource({ threadId, expectedResourceId, expectedProjectPath, }: {
threadId: string;
expectedResourceId: string;
expectedProjectPath: string;
}): Promise<AgentControllerThread>;
/** List messages for a thread (newest-`limit`, returned oldest-first), or all. */
listMessages({ threadId, limit }: {
threadId: string;
limit?: number;
}): Promise<AgentControllerMessage[]>;
/** List messages for the session's active thread (empty when not bound). */
listActiveMessages({ limit }?: {
limit?: number;
}): Promise<AgentControllerMessage[]>;
/** The first user message for a single thread, or null. */
firstUserMessage({ threadId }: {
threadId: string;
}): Promise<AgentControllerMessage | null>;
/** The first user message for each given thread id. */
firstUserMessages({ threadIds }: {
threadIds: string[];
}): Promise<Map<string, AgentControllerMessage>>;
/** Read a setting (metadata value) for the active thread. */
getSetting({ key }: {
key: string;
}): Promise<unknown>;
/** Persist a setting (metadata value) for the active thread. */
setSetting({ key, value }: {
key: string;
value: unknown;
}): Promise<void>;
/** Delete a setting (metadata value) for the active thread. */
deleteSetting({ key }: {
key: string;
}): Promise<void>;
/** Tear down the current agent subscription and reset the run tracker. */
cleanupSubscription(): void;
/**
* Ensure the session is subscribed to the given agent/thread stream, opening a
* fresh subscription (and driving its run loop) when the binding changed.
*/
ensureSubscription(threadId: string): Promise<void>;
/** Ensure a subscription for the session's active thread (no-op when unbound). */
ensureCurrentSubscription(): Promise<void>;
/** Detach from the current thread: abort the run and tear down the subscription. */
detachFromCurrent(): void;
/** Create a new thread, bind the session to it, and rebind the agent stream. */
create({ title }?: {
title?: string;
}): Promise<AgentControllerThread>;
/** Rename the session's active thread. No-op when unbound or storageless. */
rename({ title }: {
title: string;
}): Promise<void>;
/** Clone a thread (and its messages), bind the session to the clone, and rebind the stream. */
clone({ sourceThreadId, title, resourceId, }?: {
sourceThreadId?: string;
title?: string;
resourceId?: string;
}): Promise<AgentControllerThread>;
/** Switch the session to an existing thread, hydrating its persisted settings and rebinding the stream. */
switch({ threadId, emitEvent }: {
threadId: string;
emitEvent?: boolean;
}): Promise<void>;
/** Delete a thread; when it's the active thread, clear the binding and tear down the run. */
delete({ threadId }: {
threadId: string;
}): Promise<void>;
/**
* Hydrate the session's per-thread settings from the active thread's metadata:
* token usage, the persisted mode (restored first), the per-mode model, and
* observer/reflector model ids + thresholds. Best-effort: on any failure the
* token tally is reset and the rest is left at defaults.
*/
loadMetadata(): Promise<void>;
}
/**
* Owns the session's live subscription to the active thread's agent event
* stream. A subscription is created per `(agent, resource, thread)` and reused
* while that triple is unchanged (tracked by {@link key}); switching threads or
* agents tears the old one down and opens a new one.
*
* The Session owns the subscription *handle* and its dedup key plus the
* mechanical lifecycle (reuse check, teardown, identity check, run-id read).
* The AgentController still owns *how* a subscription is produced (calling the agent)
* and *how* its stream is consumed, passing the resolved handle in via
* {@link attach}.
*/
export declare class SessionStream {
#private;
waitForTeardown(signal: AbortSignal): Promise<void>;
/** Build the dedup key identifying a subscription to `threadId` for `agent`. */
static keyFor({ agent, resourceId, threadId }: {
agent: Agent;
resourceId: string;
threadId: string;
}): string;
/** Whether the open subscription already targets `key` (so it can be reused). */
matches({ key }: {
key: string;
}): boolean;
/** Adopt `subscription` as the live one, recording its dedup `key`. */
attach({ subscription, key }: {
subscription: AgentThreadSubscription<any>;
key: string;
}): void;
/** Whether a subscription is currently open. */
isOpen(): boolean;
/** Whether `subscription` is the one currently adopted (identity check). */
isCurrent({ subscription }: {
subscription: AgentThreadSubscription<any>;
}): boolean;
/** The run id the live subscription reports as active, or null when none/idle. */
activeRunId(): string | null;
/** Whether the live subscription currently has a run in flight. */
isActive(): boolean;
/** Abort the live subscription's in-flight run, if any. Swallows errors. */
abort(): void;
/** Detach the live subscription without aborting (e.g. on stream error). */
detach(): void;
/** Fully tear down the live subscription: abort, unsubscribe, and clear. */
cleanup(): void;
}
/** A tool call parked awaiting a resume, keyed in {@link SessionSuspensions}. */
export interface PendingSuspension {
/** The run id to resume when this tool call is answered. */
runId: string;
/** The suspended tool's name (e.g. `ask_user`, `submit_plan`). */
toolName: string;
}
/**
* Owns the session's parked tool suspensions: tool calls paused via the native
* tool-suspension primitive (e.g. `ask_user` / `request_access` / `submit_plan`)
* that are awaiting a resume, keyed by `toolCallId`. Each entry records the run
* id to resume and the tool name. A Map (rather than single fields) lets several
* tools — e.g. parallel `ask_user` calls in one step — stay suspended and be
* resumed independently.
*
* This is the resume *data* the AgentController reads to drive a resume. The richer
* per-suspension UI snapshot lives on the AgentController display state; the Session
* owns only what's needed to resume.
*/
export declare class SessionSuspensions {
#private;
/** Park `toolCallId` as awaiting a resume on `runId` for `toolName`. */
register({ toolCallId, runId, toolName }: {
toolCallId: string;
runId: string;
toolName: string;
}): void;
/** The parked suspension for `toolCallId`, or undefined when none. */
get({ toolCallId }: {
toolCallId: string;
}): PendingSuspension | undefined;
/** Whether `toolCallId` is currently parked. */
has({ toolCallId }: {
toolCallId: string;
}): boolean;
/** Drop `toolCallId` from the parked set (e.g. once resumed). */
delete({ toolCallId }: {
toolCallId: string;
}): void;
/** Drop all parked suspensions (e.g. on abort or thread switch). */
clear(): void;
/** Whether any tool calls are parked awaiting a resume. */
hasPending(): boolean;
/**
* Resolve which parked suspension to act on. With an explicit `toolCallId` it
* must match a parked suspension; without one it returns the single parked
* suspension (or undefined when there are zero or several).
*/
resolveToolCallId(toolCallId?: string): string | undefined;
}
/** A message queued to send once the active run finishes, held in {@link SessionFollowUps}. */
export interface FollowUp {
/** The message text to send. */
content: string;
/** Optional request context to apply when the queued message is sent. */
requestContext?: RequestContext;
}
/**
* Owns the session's follow-up queue: messages a user submits while a run is in
* progress, held FIFO until the active run finishes and the queue is drained.
*
* This owns the queue *data* (enqueue/dequeue/requeue/clear/count). The AgentController
* still drives draining — sending each message and emitting `follow_up_queued`
* as the count changes — and keeps the display-state mirror (`queuedFollowUps`).
*/
export declare class SessionFollowUps {
#private;
/** Number of messages currently queued. */
count(): number;
/** Whether the queue is empty. */
isEmpty(): boolean;
/** Append a follow-up to the back of the queue. */
enqueue(followUp: FollowUp): void;
/** Remove and return the next follow-up, or undefined when empty. */
dequeue(): FollowUp | undefined;
/** Put a follow-up back at the front (e.g. when draining it failed). */
requeue(followUp: FollowUp): void;
/** Drop all queued follow-ups (e.g. on steer or thread switch). */
clear(): void;
}
/** The decision a user returns to resolve a parked tool-approval gate. */
export interface ApprovalDecision {
/** Whether to run the gated tool or reject it. */
decision: 'approve' | 'decline';
/** Optional request context to apply when the gated tool resumes. */
requestContext?: RequestContext;
/** Optional context explaining why a tool approval was declined. */
declineContext?: {
reason?: string;
message?: string;
};
}
/**
* A user's response to a parked approval. `always_allow_category` approves the
* tool and additionally grants its category for the rest of the session.
*/
export interface ApprovalResponse {
decision: 'approve' | 'decline' | 'always_allow_category';
requestContext?: RequestContext;
declineContext?: {
reason?: string;
message?: string;
};
}
/**
* Owns the session's interactive tool-approval gate: when a tool requires user
* approval, the run parks on a promise here until the UI responds approve or
* decline. Holds the pending resolver and the name of the tool being gated.
*
* At most one approval is in flight at a time. The Session owns the gate
* mechanics (arm / resolve / clear); the AgentController still maps a decision to its
* effects (running vs declining the tool, and any "always allow" grant), since
* those touch config-derived tool categories.
*/
export declare class SessionApproval {
#private;
/**
* Park a new approval for `toolName`/`toolCallId` and return a promise that
* resolves once {@link respond} is called with the user's decision. The caller
* awaits this while the run is suspended on the gate.
*/
arm({ toolName, toolCallId }: {
toolName: string;
toolCallId?: string;
}): Promise<ApprovalDecision>;
/** Id of the tool call currently awaiting approval, or null when none. */
getToolCallId(): string | null;
/** Whether an approval is currently parked awaiting a decision. */
isArmed(): boolean;
/**
* Apply a user's {@link ApprovalResponse} to the parked gate. A no-op when
* nothing is armed. When `toolCallId` is supplied it must match the gated
* call; a mismatch is ignored so a stale/delayed response cannot resolve a
* different pending gate. `always_allow_category` runs `onAlwaysAllow` with the
* gated tool name (so the caller can grant the tool's category — a lookup that
* needs AgentController config) and then approves; `approve`/`decline` resolve as-is.
*/
respond({ decision, toolCallId, requestContext, declineContext, onAlwaysAllow, }: ApprovalResponse & {
toolCallId?: string;
onAlwaysAllow?: (toolName: string) => void;
}): void;
/**
* Release a parked gate without a user decision — used when the run is
* aborted. Resolves the awaiting producer as a `decline` so the gated tool is
* rejected (not run) and the run can finalize. A no-op when nothing is armed.
*/
cancel(): void;
/** Clear the gated tool name/call id once a parked approval has been consumed. */
clearToolName(): void;
}
/**
* Owns the session's transient run identity and abort control: the id of the
* run currently streaming on the active thread, its trace id, a monotonic
* operation counter bumped each time a new operation starts, and the
* AbortController/abort-requested flag governing cancellation. All of this is
* per-run scratch state — it is never persisted and resets between runs.
*
* The live agent subscription itself lives on {@link SessionStream}
* (`session.stream`); this holds the last run id observed on a chunk so callers
* have a stable value once the subscription has settled.
*/
export declare class SessionRun {
#private;
waitForTeardown(signal: AbortSignal): Promise<void>;
/** The current run id (null when idle). */
getRunId(): string | null;
/** Set the current run id. */
setRunId({ runId }: {
runId: string | null;
}): void;
/** The current trace id (null when unset). */
getTraceId(): string | null;
/** Set the current trace id. */
setTraceId({ traceId }: {
traceId: string | null;
}): void;
/**
* Clear all run state (run id, trace id, abort controller + requested flag)
* when a run ends or is reset. Does not touch the operation counter.
*/
reset(): void;
/** Bump and return the operation counter at the start of a new operation. */
nextOperation(): number;
/**
* Lazily create (if needed) and return the AbortController for the current
* run. Callers pass its `.signal` into the underlying stream.
*/
ensureAbortController(): AbortController;
/** Signal for the current run's AbortController, or undefined when none is armed. */
getAbortSignal(): AbortSignal | undefined;
/**
* Whether a run is currently in progress. A run is armed with an
* AbortController for its duration, so the presence of one is what "running"
* means; this is the semantic accessor callers should use.
*/
isRunning(): boolean;
/**
* Whether an AbortController is currently armed. Equivalent to
* {@link isRunning} today; kept for callers that assert on the controller's
* lifecycle specifically (e.g. that it was cleared after an abort).
*/
hasAbortController(): boolean;
/** Clear the abort-requested flag at the start of a fresh run. */
clearAbortRequested(): void;
/** Whether an abort has been requested for the current run. */
isAbortRequested(): boolean;
/**
* Request an abort: mark the run as aborting and fire the AbortController (if
* armed), then drop the controller. Leaves the requested flag set so the
* run-end path can resolve its reason as 'aborted'; {@link reset} clears it.
*/
requestAbort(): void;
}
/**
* Owns the session's currently-selected model. Source of truth for "which model
* is active", plus the per-mode model memory persisted to the thread-settings
* store (so each mode remembers the model it was last used with).
*/
export declare class SessionModel {
#private;
constructor(store: () => ThreadSettingsStore | undefined, bus: SessionBus);
/**
* Attach the AgentController-owned dependencies {@link switch} needs: the active-mode
* accessor and the optional model-use tracker. The AgentController injects these once.
*/
setResolver(options: {
getCurrentModeId: () => string;
trackModelUse?: ModelUseCountTracker;
}): void;
/** The currently-selected model id ('' when none selected yet). */
get(): string;
/** Whether a model is currently selected. */
hasSelection(): boolean;
/**
* A short display name for the selected model: the last segment of the model
* id (e.g. `__GATEWAY_ANTHROPIC_MODEL_SONNET__` -> `claude-sonnet-4-6`). Returns
* `'unknown'` when no model is selected.
*/
displayName(): string;
/** Set the in-memory selected model id (no persistence). */
set({ modelId }: {
modelId: string;
}): void;
/** Persist `modelId` as the last-used model for `modeId`. */
saveForMode({ modeId, modelId }: {
modeId: string;
modelId: string;
}): Promise<void>;
/**
* Resolve the model for `modeId`: the persisted per-mode model if present,
* else `defaultModelId`, else null.
*/
resolveForMode({ modeId, defaultModelId, }: {
modeId: string;
defaultModelId?: string;
}): Promise<string | null>;
/**
* Switch to a different model at runtime.
*
* When `scope` is `'thread'` (the default), the model is persisted as the
* per-mode model for `modeId` so it's restored when switching back. The
* in-memory selection only updates when the target mode is the active mode.
* Reports the selection to the model-use tracker and emits `model_changed`.
*/
switch({ modelId, scope, modeId, }: {
modelId: string;
scope?: 'global' | 'thread';
modeId?: string;
}): Promise<void>;
}
/**
* Owns the session's currently-selected mode and the logic for switching modes.
* Holds the active mode id and runs the version-guarded switch sequence —
* persisting the selection and coordinating the per-mode model with
* {@link SessionModel}. The AgentController still owns the mode *definitions*
* (`config.modes`); this owns "which mode is active" and how a switch unfolds.
*/
export declare class SessionMode {
#private;
constructor(store: () => ThreadSettingsStore | undefined, model: SessionModel, bus: SessionBus);
/**
* Attach the resolver that maps a mode id to its definition. The AgentController owns
* the mode catalog (`config.modes`) and injects this once.
*/
setResolver(resolve: (modeId: string) => AgentControllerMode | null): void;
/** The currently-selected mode id. */
get(): string;
/**
* Resolve the currently-selected mode id to its full definition against the
* host's mode catalog. Throws if the selected mode id isn't in the catalog.
*/
resolve(): AgentControllerMode;
/** Set the currently-selected mode id (on default resolution or hydration). */
set({ modeId }: {
modeId: string;
}): void;
/**
* Switch to a different mode.
*
* Emits `mode_changed`, then runs the version-guarded sequence: remember the
* outgoing mode's model, persist the new mode, then resolve and apply the
* incoming mode's model — emitting `model_changed` once applied. A newer
* switch starting mid-flight supersedes this one, which then bails before
* emitting `model_changed`.
*/
switch({ modeId }: {
modeId: string;
}): Promise<void>;
}
/** Per-role wiring + state/config keys a {@link SessionOMRole} reads and writes. */
interface SessionOMRoleConfig {
/** The event `role` and `om_model_changed` discriminator for this role. */
role: 'observer' | 'reflector';
/** Session-state / thread-settings key holding this role's model id. */
modelIdKey: 'observerModelId' | 'reflectorModelId';
/** Session-state key holding this role's threshold. */
thresholdKey: 'observationThreshold' | 'reflectionThreshold';
/** Resolve this role's default model id from `omConfig`. */
defaultModelId: (omConfig: AgentControllerOMConfig | undefined) => string | undefined;
/** Resolve this role's default threshold from `omConfig`. */
defaultThreshold: (omConfig: AgentControllerOMConfig | undefined) => number | undefined;
}
/**
* One observational-memory role (observer or reflector): its model id, resolved
* model instance, threshold, and model switch. Reads return the session-state
* value when set, falling back to the AgentController's `omConfig` defaults. The shared
* wiring is injected by {@link SessionOM.setResolver}.
*/
declare class SessionOMRole {
#private;
constructor(config: SessionOMRoleConfig, bus: SessionBus);
/** @internal Injected by {@link SessionOM.setResolver}. */
setWiring(wiring: {
getState: () => Record<string, unknown>;
setState: (updates: Record<string, unknown>) => void;
setSetting: (args: {
key: string;
value: unknown;
}) => Promise<void>;
omConfig?: AgentControllerOMConfig;
gateways?: MastraModelGatewayInterface[];
}): void;
/** This role's model id from session state, falling back to `omConfig`. */
modelId(): string | undefined;
/** This role's threshold from session state, falling back to `omConfig`. */
threshold(): number | undefined;
/**
* Resolve this role's model id to a model instance via the configured
* gateways, or undefined when unset. The bare model id string is routed
* through {@link ModelRouterLanguageModel}, which selects the matching
* gateway (or the built-in defaults) and resolves provider auth.
*/
resolvedModel(): MastraModelConfig | undefined;
/** Switch this role's model: update session state, persist, and emit. */
switchModel({ modelId }: {
modelId: string;
}): Promise<void>;
}
/**
* Owns the session's observational-memory model selection, grouped by role:
* {@link SessionOM.observer} and {@link SessionOM.reflector}. The AgentController owns
* `omConfig` and the model resolver, so it injects them — plus the session-state
* read/write and thread-settings persistence — once via {@link setResolver},
* which fans the wiring out to both roles.
*/
declare class SessionOM {
readonly observer: SessionOMRole;
readonly reflector: SessionOMRole;
constructor(bus: SessionBus);
/**
* Attach the session-state read/write, thread-settings persistence, and the
* AgentController-owned `omConfig` defaults plus model resolver. The AgentController injects
* these once; the wiring is shared by both roles.
*/
setResolver(options: {
getState: () => Record<string, unknown>;
setState: (updates: Record<string, unknown>) => void;
setSetting: (args: {
key: string;
value: unknown;
}) => Promise<void>;
omConfig?: AgentControllerOMConfig;
gateways?: MastraModelGatewayInterface[];
}): void;
}
/**
* Owns the session's tool-permission rules: the per-category and per-tool
* approval policies persisted in session state under `permissionRules`. The
* AgentController injects the session-state read/write once via {@link setResolver}.
*
* These are the persisted rules consulted during tool-approval resolution; they
* are distinct from the in-memory "allow for this session" grants on the
* Session.
*/
declare class SessionPermissions {
#private;
/** Attach the session-state read/write. The AgentController injects these once. */
setResolver(options: {
getState: () => Record<string, unknown>;
setState: (updates: Record<string, unknown>) => Promise<void>;
}): void;
/** The current permission rules, or empty rules when none are set. */
getRules(): PermissionRules;
/** Set the approval policy for a tool category. Resolves once persisted. */
setForCategory({ category, policy }: {
category: ToolCategory;
policy: PermissionPolicy;
}): Promise<void>;
/** Set the approval policy for an individual tool. Resolves once persisted. */
setForTool({ toolName, policy }: {
toolName: string;
policy: PermissionPolicy;
}): Promise<void>;
}
/**
* The subagent model selection. Reads prefer the per-`agentType` value and fall
* back to the global subagent model; writes persist to thread settings and emit
* a `subagent_model_changed` event. Wiring is injected by
* {@link SessionSubagents.setResolver}.
*/
declare class SessionSubagentModel {
#private;
constructor(bus: SessionBus);
/** @internal Injected by {@link SessionSubagents.setResolver}. */
setWiring(wiring: {
getState: () => Record<string, unknown>;
setState: (updates: Record<string, unknown>) => void;
setSetting: (args: {
key: string;
value: unknown;
}) => Promise<void>;
}): void;
/**
* The subagent model id, preferring the `agentType`-specific value when one is
* given, then the global subagent model, or `null` when neither is set.
*/
get({ agentType }?: {
agentType?: string;
}): string | null;
/**
* Set the subagent model id (per-`agentType` when given, otherwise global).
* Persists to thread settings and emits `subagent_model_changed`.
*/
set({ modelId, agentType }: {
modelId: string;
agentType?: string;
}): Promise<void>;
}
/**
* The session's subagent configuration. Currently exposes the subagent model
* selection under {@link SessionSubagents.model}; grouped under `subagents` to
* leave room for additional subagent settings. The AgentController injects the
* session-state read/write, thread-settings persistence, and event emitter once
* via {@link setResolver}.
*/
declare class SessionSubagents {
readonly model: SessionSubagentModel;
constructor(bus: SessionBus);
/**
* Attach the session-state read/write and thread-settings persistence. The
* AgentController injects these once.
*/
setResolver(options: {
getState: () => Record<string, unknown>;
setState: (updates: Record<string, unknown>) => void;
setSetting: (args: {
key: string;
value: unknown;
}) => Promise<void>;
}): void;
}
interface SessionStateOptions<TState> {
initialState?: Partial<TState>;
stateSchema?: PublicSchema<TState, any>;
}
/**
* A AgentController session owns the per-conversation runtime state that today lives
* flattened on the {@link AgentController} instance. This class is the seam we extract
* that state into, one concern at a time, so the AgentController can eventually own a
* `Session` rather than the state itself.
*
* Currently owns:
* - the live AgentController state (`session.state`): schema-validated snapshots and
* serialized updates that emit `state_changed`.
* - session-scoped permission grants — the "allow for this session" approvals a
* user makes when a tool or tool category is gated behind the permission check.
* - the live token-usage counter for the active thread. The Session holds the
* in-memory running tally; the AgentController remains responsible for persisting it
* to (and hydrating it from) thread metadata, because usage is thread-scoped.
* - the currently-selected mode (`session.mode`) and model (`session.model`).
* The Session is the source of truth for which mode/model is active and owns
* the mode-switch sequence and per-mode model memory. The AgentController still owns
* the mode *definitions* (`config.modes`).
* - transient run identity and abort control (`session.run`): the current run
* id, trace id, monotonic operation counter, and the AbortController/
* abort-requested flag. This is per-run scratch state and is never persisted.
* - the live agent thread subscription (`session.stream`): the open
* subscription to the active thread's event stream and its dedup key. The
* AgentController still produces the subscription (calling the agent) and consumes its
* stream; the Session owns the handle and its lifecycle.
* - the parked tool suspensions (`session.suspensions`): tool calls paused via
* the native tool-suspension primitive awaiting a resume, keyed by toolCallId.
* The Session owns the resume data; the AgentController keeps the richer per-suspension
* UI snapshot on its display state.
* - the follow-up queue (`session.followUps`): messages a user submits while a
* run is in progress, held FIFO until the run finishes. The Session owns the
* queue; the AgentController drives draining and keeps the `queuedFollowUps` display
* mirror.
* - the interactive tool-approval gate (`session.approval`): when a tool needs
* user approval, the run parks on a promise here until the UI responds. The
* Session owns the gate; the AgentController maps the decision to its effects (run vs
* decline, any "always allow" grant), which touch config-derived categories.
*
* It also exposes a couple of accessors that compose `run` and `stream`:
* {@link getCurrentRunId} (the active run id, preferring the live subscription)
* and {@link abortRun} (abort the live run and mark it aborting).
*
* Mode/model persistence is thread-scoped, so the Session writes through a
* {@link ThreadSettingsStore} the AgentController backs with thread metadata; when no
* storage is configured the store is absent and state stays in memory.
*/
/**
* Owns the session's canonical display state — the projection a UI renders from
* instead of folding raw events itself. The Session holds the snapshot and the
* reducer ({@link apply}) that keeps it in sync with every AgentController event; the
* AgentController still owns the event bus and dispatches `display_state_changed` to
* listeners after applying.
*
* The reducer needs a few read-only host/session facts it doesn't own: the live
* token-usage tally, a subagent display-name lookup (AgentController config), and the
* active thread id (to decide whether a `thread_deleted` clears the view). Those
* are injected at construction so the reducer stays self-contained.
*/
export declare class SessionDisplayState {
#private;
private readonly deps;
constructor(deps: {
/** The session's live token-usage tally, mirrored into the view on usage/thread events. */
getTokenUsage: () => TokenUsage;
/** Resolve a subagent's display name from AgentController config, or undefined when unnamed. */
getSubagentDisplayName: (agentType: string) => string | undefined;
/** The active thread id, used to gate `thread_deleted` resets. */
getThreadId: () => string | null;
/** Clear the session's follow-up queue when thread-scoped display state resets. */
clearFollowUps: () => void;
});
/**
* A read-only snapshot of the canonical display state. UIs should render from
* this instead of building state up from raw events.
*/
get(): Readonly<AgentControllerDisplayState>;
/**
* Drop the display mirror of every parked tool suspension. Used on abort,
* which abandons the run's parked suspensions; the caller dispatches
* `display_state_changed`.
*/
clearPendingSuspensions(): void;
/**
* Clear the modified-files tally without touching the rest of the snapshot.
* Used after a clone, which starts the cloned thread with a clean working set
* while the surrounding UI reset handles tasks/tools explicitly.
*/
clearModifiedFiles(): void;
/**
* Drop the display mirror of a single parked tool suspension once it has been
* resumed, so the UI stops rendering only the resolved prompt while any other
* parked suspensions stay visible.
*/
deletePendingSuspension(toolCallId: string): void;
/**
* Restore task display state after a UI replays persisted task-tool history.
* Updates the snapshot without emitting a live `task_updated` event, since no
* task tool just ran. The caller dispatches `display_state_changed`.
*/
restoreTasks(tasks: TaskItemSnapshot[]): void;
/**
* Reset display fields scoped to a thread. Called on thread switch/creation.
* Also clears the session's follow-up queue (mirrored by `queuedFollowUps`).
*/
resetThread(): void;
/**
* Apply a display-state update based on an incoming event. The centralized
* state machine that keeps {@link AgentControllerDisplayState} in sync with every
* event the AgentController emits.
*/
apply(event: AgentControllerEvent): void;
}
/**
* A session's event bus. Owns the listener list and the full emit pipeline:
* fold the event into the canonical display state, dispatch to this session's
* listeners, then fan out a synthetic `display_state_changed`. Each session
* has its own bus, so events never cross between sessions. Subsystems hold a
* reference to their session's bus and call {@link emit} directly.
*/
export declare class SessionBus {
#private;
/** Attach the display-state reducer the bus folds events into. Set once by the Session. */
setDisplayState(displayState: SessionDisplayState): void;
subscribe(listener: AgentControllerEventListener): () => void;
emit(event: AgentControllerEvent): void;
}
export declare class Session<TState = unknown> {
#private;
/** The session's currently-selected model (source of truth) + per-mode memory. */
readonly model: SessionModel;
/** The session's currently-selected mode and switch sequence. */
readonly mode: SessionMode;
/** The session's observational-memory model selection (observer/reflector). */
readonly om: SessionOM;
/** The session's persisted tool-permission rules (per-category / per-tool). */
readonly permissions: SessionPermissions;
/** The session's subagent configuration (currently the subagent model). */
readonly subagents: SessionSubagents;
/** Transient run identity (run id, trace id, operation counter) for the active run. */
readonly run: SessionRun;
/** Live subscription to the active thread's agent event stream. */
readonly stream: SessionStream;
/** Tool calls parked awaiting a resume (the resume data, keyed by toolCallId). */
readonly suspensions: SessionSuspensions;
/** Messages queued to send after the active run finishes. */
readonly followUps: SessionFollowUps;
/** The interactive tool-approval gate the current run parks on. */
readonly approval: SessionApproval;
/** The session's identity: the memory resourceId it reads/writes under. */
readonly identity: SessionIdentity;
/** The session's thread domain: current binding + reads scoped to it. */
readonly thread: SessionThread;
/** The canonical display state a UI renders, plus the reducer that maintains it. */
readonly displayState: SessionDisplayState;
/** The session-owned AgentController state domain. */
readonly state: AgentControllerRequestState<TState>;
browser?: MastraBrowser;
constructor({ resourceId, state, id, ownerId, tags, workspace, browser, }: {
resourceId: string;
state?: SessionStateOptions<TState>;
id: string;
ownerId: string;
tags?: Record<string, string>;
workspace: Workspace;
browser?: MastraBrowser;
});
/**
* This session's scoping tags (e.g. `{ projectPath }`), stamped onto every
* thread it creates. Returns a copy; empty when the session is unscoped.
*/
getTags(): Record<string, string>;
/**
* Subscribe to this session's events. Returns an unsubscribe function.
* Listeners are scoped to this session: a session never delivers its events
* to another session's subscribers.
*/
subscribe(listener: AgentControllerEventListener): () => void;
/**
* Emit an event on this session. Delegates to this session's bus, which folds
* the event into the canonical display state, dispatches to this session's
* listeners, then fans out a synthetic `display_state_changed`.
*/
emit(event: AgentControllerEvent): void;
/**
* Attach the thread-settings store the Session persists mode/model through.
* The AgentController calls this once storage is available; without it, mode/model
* state lives purely in memory.
*/
setStore(store: ThreadSettingsStore | undefined): void;
/**
* Attach the tool→category resolver used when a user picks "always allow
* category". The category map is AgentController config, so the AgentController injects this
* once; without it, an "always_allow_category" decision simply approves.
*/
setCategoryResolver(resolveCategory: (toolName: string) => ToolCategory | null): void;
/**
* Attach the subagent display-name resolver the display-state reducer uses to
* label active subagents. The subagent catalog is AgentController config, so the
* AgentController injects this once; without it, subagents render without a name.
*/
setSubagentNameResolver(resolveSubagentName: (agentType: string) => string | undefined): void;
/**
* Attach the AgentController-owned run machinery this session leverages to drive agent
* runs (resolve the agent, build run/stream options + toolsets + request
* context, persist usage, generate ids). The AgentController injects this once when it
* constructs the session. The run loop, run state, and thread stream live on
* the session; this is the narrow set of shared capabilities it reaches back
* into the host for — see {@link SessionMachinery}.
*/
setMachinery(machinery: SessionMachinery): void;
/**
* The AgentController-owned run machinery injected via {@link setMachinery}, throwing
* when accessed before wiring (a run can never be driven without it).
*/
get machinery(): SessionMachinery;
/** The per-session run engine, throwing when accessed before machinery is wired. */
get runEngine(): SessionRunEngine;
/**