Harness class
The Harness feature is in alpha stage and subject to breaking changes in minor versions until it graduates from its alpha status.
The Harness class orchestrates multiple agent modes, shared state, memory, and storage. It provides a control layer that a TUI or other UI can drive to manage threads, switch models and modes, send messages, handle tool approvals, and track events.
Per-conversation state — identity, the active thread, mode and model selection, run state, grants, and the display snapshot — lives on the Session, accessed through harness.session.
For a conceptual introduction, see the Harness overview.
Usage exampleDirect link to Usage example
Import the Harness class and create a new instance with your agent, storage backend, and modes:
import { Harness } from '@mastra/core/harness'
import { LibSQLStore } from '@mastra/libsql'
import { z } from 'zod'
const harness = new Harness({
id: 'my-coding-agent',
agent: myAgent,
storage: new LibSQLStore({ url: 'file:./data.db' }),
stateSchema: z.object({
currentModelId: z.string().optional(),
}),
modes: [
{
id: 'plan',
name: 'Plan',
metadata: { default: true },
instructions: 'Reason about changes before making them.',
},
{ id: 'build', name: 'Build', transitionsTo: 'plan' },
],
})
harness.subscribe(event => {
if (event.type === 'message_update') {
renderMessage(event.message)
}
})
await harness.init()
await harness.selectOrCreateThread()
await harness.sendMessage({ content: 'Hello!' })
Constructor parametersDirect link to Constructor parameters
id:
resourceId?:
storage?:
stateSchema?:
initialState?:
memory?:
modes:
id:
name?:
default?:
defaultModelId?:
description?:
instructions?:
transitionsTo?:
metadata?:
tools?:
additionalTools?:
agent?:
agent?:
defaultModeId?:
instructions?:
tools?:
workspace?:
browser?:
subagents?:
id:
name:
description:
instructions:
tools?:
allowedHarnessTools?:
allowedWorkspaceTools?:
defaultModelId?:
maxSteps?:
stopWhen?:
forked?:
resolveModel?:
omConfig?:
disableBuiltinTools?:
heartbeatHandlers?:
idGenerator?:
modelAuthChecker?:
modelUseCountProvider?:
modelUseCountTracker?:
customModelCatalogProvider?:
gateways?:
toolCategoryResolver?:
pubsub?:
threadLock?:
observability?:
PropertiesDirect link to Properties
id:
session:
MethodsDirect link to Methods
LifecycleDirect link to Lifecycle
init()Direct link to init
Initialize the harness. Loads storage, initializes the workspace, propagates memory and workspace to mode agents, and starts heartbeat handlers. Call this before using the harness.
await harness.init()
selectOrCreateThread()Direct link to selectorcreatethread
Select the most recent thread for the current resource, or create one if none exist. Loads thread metadata and acquires a thread lock.
const thread = await harness.selectOrCreateThread()
destroy()Direct link to destroy
Stop all heartbeat handlers and clean up resources.
await harness.destroy()
removeHeartbeat({ id })Direct link to removeheartbeat-id-
Remove a specific heartbeat handler by ID. Calls the handler's shutdown() callback if defined.
await harness.removeHeartbeat({ id: 'gateway-sync' })
stopHeartbeats()Direct link to stopheartbeats
Stop and remove all heartbeat handlers.
await harness.stopHeartbeats()
getCurrentAgent()Direct link to getcurrentagent
Return the fully-configured Agent for the current mode with runtime services (storage, memory, workspace, pubsub, telemetry) propagated.
const agent = harness.getCurrentAgent()
getResolvedMemory()Direct link to getresolvedmemory
Return the resolved memory instance, or null if no memory is configured.
const memory = await harness.getResolvedMemory()
getMastra()Direct link to getmastra
Return the internal Mastra instance, or undefined before init(). Useful for scorer registration, observability access, and eval tooling.
const mastra = harness.getMastra()
ModesDirect link to Modes
listModes()Direct link to listmodes
Return all configured HarnessMode instances.
const modes = harness.listModes()
To read the active mode, use session.mode.get(); to resolve it to a full HarnessMode, use session.mode.resolve().
ModelsDirect link to Models
To read the active model ID, use session.model.get(); for a short display name, use session.model.displayName(); to check whether a model is selected, use session.model.hasSelection().
getCurrentModelAuthStatus()Direct link to getcurrentmodelauthstatus
Check if the current model's provider has authentication configured. Uses modelAuthChecker if provided, falling back to environment variable checks from the provider registry.
const status = await harness.getCurrentModelAuthStatus()
// { hasAuth: true, apiKeyEnvVar: 'ANTHROPIC_API_KEY' }
listAvailableModels()Direct link to listavailablemodels
Retrieve all available models from the provider registry, including their authentication status and use counts.
const models = await harness.listAvailableModels()
// [{ id, provider, modelName, hasApiKey, apiKeyEnvVar, useCount }]
ThreadsDirect link to Threads
The harness owns thread lifecycle transitions — creating, switching, cloning, renaming, and deleting threads — because they coordinate the shared thread lock and emit events. The active thread binding and thread/message reads live on session.thread.
createThread({ title? })Direct link to createthread-title-
Create a new thread. Initializes thread metadata, saves it to storage, acquires a thread lock, and emits a thread_created event.
const thread = await harness.createThread({ title: 'New conversation' })
switchThread({ threadId })Direct link to switchthread-threadid-
Switch to a different thread. Aborts any in-progress operations, acquires a lock on the new thread, releases the lock on the previous thread, loads the thread's metadata, and emits a thread_changed event.
await harness.switchThread({ threadId: 'thread-abc123' })
To list threads from storage, use session.thread.list(). By default it returns only threads for the current resource and hides transient forked subagent threads; pass includeForkedSubagents: true to opt back into seeing them — e.g. for a debug panel.
renameThread({ title })Direct link to renamethread-title-
Update the title of the current thread.
await harness.renameThread({ title: 'Updated title' })
cloneThread({ sourceThreadId?, title?, resourceId? })Direct link to clonethread-sourcethreadid-title-resourceid-
Clone an existing thread and switch to the clone. Copies all messages, acquires a lock on the new thread, releases the lock on the previous thread, and emits a thread_created event. If sourceThreadId is omitted, the current thread is cloned. When Observational Memory is enabled, OM records are cloned with remapped message IDs.
// Clone the current thread
const cloned = await harness.cloneThread()
// Clone a specific thread with a custom title
const cloned = await harness.cloneThread({
sourceThreadId: 'thread-abc123',
title: 'Alternative approach',
})
See Memory.cloneThread() for details on what gets cloned.
To read the current resource ID, use session.identity.getResourceId().
setResourceId({ resourceId })Direct link to setresourceid-resourceid-
Set the resource ID and clear the current thread.
harness.setResourceId({ resourceId: 'project-xyz' })
getKnownResourceIds()Direct link to getknownresourceids
Return the distinct resource IDs that have threads in storage.
const resourceIds = await harness.getKnownResourceIds()
getSession()Direct link to getsession
Return current session information including thread ID, mode ID, and the list of threads.
const session = await harness.getSession()
// { currentThreadId, currentModeId, threads }
MessagesDirect link to Messages
sendMessage({ content, files?, requestContext? })Direct link to sendmessage-content-files-requestcontext-
Send a message to the current agent. Creates a thread if none exists, builds a RequestContext and toolsets, and streams the agent's response. Handles tool calls, approvals, and errors automatically. If you provide requestContext, the harness forwards it to tools and subagents during the run.
await harness.sendMessage({ content: 'Explain the authentication flow' })
Reading messages is owned by session.thread: use listActiveMessages() for the active thread, listMessages({ threadId }) for a specific thread, and firstUserMessage({ threadId }) / firstUserMessages({ threadIds }) for thread previews.
MemoryDirect link to Memory
The memory property bundles thread management operations into a single namespace. memory.createThread, memory.switchThread, and memory.renameThread delegate to the corresponding Harness lifecycle methods documented above; memory.listThreads delegates to session.thread.list().
memory.deleteThread({ threadId })Direct link to memorydeletethread-threadid-
Delete a thread and all its messages from storage. If the deleted thread is the currently active thread, the thread lock is released and the harness clears its active thread. Emits a thread_deleted event.
await harness.memory.deleteThread({ threadId: 'thread-abc123' })
Flow controlDirect link to Flow control
abort()Direct link to abort
Abort any in-progress generation.
harness.abort()
steer({ content, requestContext? })Direct link to steer-content-requestcontext-
Steer the agent mid-stream by injecting an instruction into the current generation.
harness.steer({ content: 'Focus on security implications' })
followUp({ content, requestContext? })Direct link to followup-content-requestcontext-
Queue a follow-up message to be sent after the current generation completes. If no operation is running, sends the message immediately.
harness.followUp({ content: 'Now apply those changes' })
Tool approvalsDirect link to Tool approvals
Responding to a pending tool approval is owned by the session — see session.respondToToolApproval(). The harness owns the permission policy that decides when approval is required, documented under Permissions below.
Tool suspensions and plansDirect link to Tool suspensions and plans
respondToToolSuspension({ resumeData, toolCallId?, requestContext? })Direct link to respondtotoolsuspension-resumedata-toolcallid-requestcontext-
Respond to a pending tool suspension. Interactive built-in tools such as ask_user and request_access pause through the native tool-suspension primitive, which emits a tool_suspended event carrying toolCallId, toolName, and suspendPayload. Pass resumeData to resume the suspended tool with the user's response.
Provide toolCallId to select which suspension to resume. It is required when more than one tool is suspended at the same time (for example, parallel ask_user calls). When omitted, it resolves to the sole pending suspension.
harness.subscribe(event => {
if (event.type === 'tool_suspended' && event.toolName === 'ask_user') {
const { question } = event.suspendPayload as { question: string }
// Show `question` to the user, then resume the tool with their answer.
harness.respondToToolSuspension({
toolCallId: event.toolCallId,
resumeData: 'Yes, proceed with the refactor',
})
}
})
For multi-select questions, pass the selected option labels as a string array.
harness.respondToToolSuspension({
toolCallId: event.toolCallId,
resumeData: ['Add tests', 'Update docs'],
})
Responding to a submitted planDirect link to Responding to a submitted plan
The submit_plan built-in tool pauses via the native tool-suspension primitive, so it surfaces through the same tool_suspended event as other interactive tools. Resume it with respondToToolSuspension, passing a resumeData object with action ('approved' or 'rejected') and an optional feedback string.
On approval, the Harness automatically switches to its default (execution) mode. On rejection, the plan-mode run resumes so the agent can revise and submit again.
harness.respondToToolSuspension({
toolCallId: event.toolCallId,
resumeData: { action: 'approved' },
})
harness.respondToToolSuspension({
toolCallId: event.toolCallId,
resumeData: { action: 'rejected', feedback: 'Needs more detail' },
})
PermissionsDirect link to Permissions
getToolCategory({ toolName })Direct link to gettoolcategory-toolname-
Resolve a tool's category using the configured toolCategoryResolver.
const category = harness.getToolCategory({ toolName: 'mastra_workspace_write_file' })
// 'edit'
WorkspaceDirect link to Workspace
getWorkspace()Direct link to getworkspace
Return the current workspace instance, or undefined if no workspace is configured or it hasn't been resolved yet.
const workspace = harness.getWorkspace()
resolveWorkspace({ requestContext? })Direct link to resolveworkspace-requestcontext-
Eagerly resolve and cache the workspace. For dynamic workspaces (factory function), this triggers the factory and caches the result so getWorkspace() returns it. Returns the resolved workspace or undefined if none is configured.
const workspace = await harness.resolveWorkspace()
hasWorkspace()Direct link to hasworkspace
Return whether a workspace is configured (static, config-based, or dynamic).
if (harness.hasWorkspace()) {
const workspace = await harness.resolveWorkspace()
}
isWorkspaceReady()Direct link to isworkspaceready
Return whether the workspace is ready to use. For dynamic workspaces (factory function), always returns true. For static workspaces, returns true after init() succeeds.
if (harness.isWorkspaceReady()) {
const workspace = harness.getWorkspace()
}
destroyWorkspace()Direct link to destroyworkspace
Destroy the workspace and release resources. Only applies to static workspaces — dynamic workspaces aren't destroyed.
await harness.destroyWorkspace()
Observational MemoryDirect link to Observational Memory
loadOMProgress()Direct link to loadomprogress
Load observational memory records for the current thread and emit an om_status event with reconstructed progress.
await harness.loadOMProgress()
getObservationalMemoryRecord()Direct link to getobservationalmemoryrecord
Return the full ObservationalMemoryRecord for the current thread and resource, or null if no thread is selected or no record exists.
const record = await harness.getObservationalMemoryRecord()
if (record) {
console.log(record.activeObservations)
console.log(record.generationCount)
console.log(record.observationTokenCount)
}
The observer/reflector model selection and observation/reflection thresholds live on the Session under session.om, grouped by role. Read them with session.om.observer.modelId() / session.om.reflector.modelId() and session.om.observer.threshold() / session.om.reflector.threshold(), and change a role's model with session.om.observer.switchModel() / session.om.reflector.switchModel().
Forked subagentsDirect link to Forked subagents
By default, a subagent runs with a fresh context — it doesn't see the parent conversation. Forked subagents opt into a different model: the subagent runs on a clone of the parent thread and reuses the parent agent's full configuration. This is useful when the subagent needs the full context of the conversation so far (e.g., recalling earlier user-supplied facts), and when prompt-cache hit rates matter.
Enabling forked modeDirect link to Enabling forked mode
Set forked: true either on the HarnessSubagent definition (per-type default) or on each subagent tool call (per-invocation override):
// Per-type default — every call to this subagent forks unless overridden.
const subagents: HarnessSubagent[] = [
{
id: 'collaborator',
name: 'Collaborator',
description: 'Continues the conversation in a fork to try a different angle.',
instructions: '...',
forked: true,
},
]
The model can also pass forked: true (or forked: false) per-invocation in the subagent tool input; the per-invocation value wins.
Semantics and constraintsDirect link to Semantics and constraints
- Memory required. Forked mode calls
memory.cloneThreadto create the fork, so the harness must havememoryconfigured and an active parent thread. Calls without those return a structured error rather than throwing. - Parent agent reused. The fork runs through the parent agent's
stream(...)call. The parent's instructions, tools, model,maxSteps, andstopWhenapply. The subagent definition'sinstructions,tools,allowedHarnessTools,allowedWorkspaceTools,defaultModelId,maxSteps, andstopWhenare ignored in forked mode — this is what preserves the prompt-cache prefix. - Toolsets inherited, recursive forks blocked at runtime. Forks inherit the parent's toolsets verbatim (
ask_user,submit_plan, user-configured harness tools, including thesubagenttool itself) so the LLM request prefix — system prompt + tool list + tool schemas + tool descriptions — stays byte-identical to the parent's. This is what preserves the prompt cache. Thesubagententry is kept on the model side but itsexecuteis replaced inside the fork with a stub that returns a non-error "tool unavailable inside a forked subagent" message: nested forks are blocked at the runtime layer without perturbing the cached prefix. - Fork threads are tagged. Each fork thread is created with
metadata.forkedSubagent === trueandmetadata.parentThreadId === <parent>. By default,session.thread.list()hides these so they don't show up in user-facing thread pickers / startup flows. PassincludeForkedSubagents: trueto see them in admin / debug tooling. - Save-queue flushed before clone. The agent stream batches message saves through a debounced
SaveQueueManager, so the parent's latest user / assistant turn may not be on disk yet when the subagent tool call fires. The fork tool flushes pending saves first via theflushMessagescallback onAgentToolExecutionContextbefore cloning, so the fork actually carries the latest turn. Flush failures are non-fatal — the clone still runs. - Parent thread untouched. All subagent activity (messages, OM writes) lands on the fork. The parent thread is never appended to during a forked subagent run.
When to prefer non-forked modeDirect link to When to prefer non-forked mode
Forked mode trades isolation for context inheritance. If the subagent should run with a strictly smaller toolset, a different system prompt, or a cheaper model, use the default (non-forked) mode and pass any required context explicitly in the task description.
EventsDirect link to Events
subscribe(listener)Direct link to subscribelistener
Register an event listener. Returns an unsubscribe function.
Use this method for all consumers — UI, Server-Sent Events (SSE), terminal UI (TUI), bridge rendering, audit logs, debugging, analytics, and deterministic replay. For display rendering, watch for the display_state_changed event and read the latest snapshot from session.displayState.get(). After every event the harness emits display_state_changed, so high-frequency events such as message_update, tool_update, and tool_input_delta are coalesced into the next snapshot.
// Render from the coalesced display-state snapshot:
const unsubscribe = harness.subscribe(event => {
if (event.type === 'display_state_changed') {
render(harness.session.displayState.get())
}
})
// Render an initial frame before the next harness event:
render(harness.session.displayState.get())
To handle raw events directly, switch on event.type:
const unsubscribe = harness.subscribe(event => {
switch (event.type) {
case 'message_update':
renderMessage(event.message)
break
case 'tool_approval_required':
showApprovalPrompt(event.toolName)
break
case 'error':
console.error(event.error)
break
}
})
// Later:
unsubscribe()
EventsDirect link to Events
The harness emits events through registered listeners. The following table lists the available event types:
| Event type | Description |
|---|---|
mode_changed | The active mode changed. |
model_changed | The active model changed. |
thread_changed | The active thread changed. |
thread_created | A new thread was created. |
thread_deleted | A thread was deleted. |
state_changed | Harness state was updated. |
agent_start | The agent started processing. |
agent_end | The agent finished processing. |
message_start | A new message started streaming. |
message_update | A message was updated with new content. |
message_end | A message finished streaming. |
tool_start | A tool call started. |
tool_approval_required | A tool call requires user approval. |
tool_suspended | A tool paused via the native tool-suspension primitive (for example ask_user, request_access, or submit_plan). Includes toolCallId, toolName, and suspendPayload. Resume it with respondToToolSuspension. |
tool_update | A tool call was updated with progress. |
tool_end | A tool call finished. |
tool_input_start | Tool input started streaming. |
tool_input_delta | Tool input received a streaming delta. |
tool_input_end | Tool input finished streaming. |
usage_update | Token usage was updated. |
error | An error occurred. |
info | An informational message was emitted. |
system_reminder | A system reminder was injected into the conversation. |
state_signal | A state signal was emitted (state-driven prompt injection). |
reactive_signal | A reactive signal fired in response to a state change. |
notification | A notification was delivered to the thread inbox. |
notification_summary | A summary of pending notifications was emitted. |
follow_up_queued | A follow-up message was queued. |
workspace_status_changed | The workspace status changed. |
workspace_ready | The workspace finished initializing. |
workspace_error | The workspace encountered an error. |
om_status | Observational Memory status update. |
om_activation | Observational Memory was activated for the thread. |
om_model_changed | An Observational Memory role model changed (observer or reflector). |
om_observation_start | An observation started. |
om_observation_end | An observation completed. |
om_observation_failed | An observation failed. |
om_reflection_start | A reflection started. |
om_reflection_end | A reflection completed. |
om_reflection_failed | A reflection failed. |
om_buffering_start | Observational Memory started buffering messages. |
om_buffering_end | Observational Memory finished buffering messages. |
om_buffering_failed | Observational Memory buffering failed. |
om_thread_title_updated | Observational Memory updated the thread title. |
subagent_start | A subagent started processing. |
subagent_text_delta | A subagent emitted a text delta. |
subagent_tool_start | A subagent started a tool call. |
subagent_tool_end | A subagent finished a tool call. |
subagent_end | A subagent finished processing. |
subagent_model_changed | A subagent's model changed. |
task_updated | A task list was updated. |
goal_evaluation | A goal evaluation completed (when native agent goals are configured). |
shell_output | A tool emitted shell output (stdout or stderr). |
display_state_changed | The canonical HarnessDisplayState snapshot changed. Read it from session.displayState.get(). |
The harness also emits low-level streaming content chunks — text, thinking, tool_call, tool_result, image, and file. These are the raw pieces that get assembled into messages; most UIs render from message_update (or read the session.displayState snapshot) rather than subscribing to them directly.
Built-in toolsDirect link to Built-in tools
The harness provides built-in tools to agents in every mode:
| Tool | Description |
|---|---|
ask_user | Ask the user a question and wait for their response. Supports free text, single-select choices, and multi-select choices. |
submit_plan | Submit a plan for user review and approval. |
task_write | Create or replace a structured task list for tracking progress. Assigns task IDs when omitted and returns the structured task list snapshot. |
task_update | Update one tracked task by ID and return the structured task list snapshot. |
task_complete | Mark one tracked task completed by ID and return the structured task list snapshot. |
task_check | Check the completion status of the current task list and return tasks, summary, incompleteTasks, and isError fields. |
subagent | Spawn a focused subagent with constrained tools (only available when subagents is configured). Pass forked: true to inherit the parent conversation — see Forked subagents. |
ask_user selectionsDirect link to ask_user-selections
The ask_user tool accepts options for choice prompts. Set selectionMode to single_select to let the user pick one option, or multi_select to let the user pick multiple options. When options are provided and selectionMode is omitted, the prompt defaults to single_select. Omit options for free-text questions.
The following example demonstrates a multi-select response handler. The tool pauses through the tool_suspended event, the UI reads selectionMode from event.suspendPayload, lets the user choose multiple options, then returns a string array with respondToToolSuspension().
harness.subscribe(event => {
if (event.type === 'tool_suspended' && event.toolName === 'ask_user') {
const { selectionMode } = event.suspendPayload as { selectionMode?: string }
if (selectionMode === 'multi_select') {
harness.respondToToolSuspension({
toolCallId: event.toolCallId,
resumeData: ['Add tests', 'Update docs'],
})
}
}
})