Skip to main content

Harness class

alpha

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 example
Direct link to Usage example

Import the Harness class and create a new instance with your agent, storage backend, and modes:

src/mastra/harness.ts
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 parameters
Direct link to Constructor parameters

id:

string
Unique identifier for this harness instance.

resourceId?:

string
Resource ID for grouping threads (e.g., project identifier). Threads are scoped to this resource ID. Defaults to `id`.

storage?:

MastraCompositeStore
Storage backend for persistence (threads, messages, state).

stateSchema?:

StandardJSONSchemaV1
Standard JSON Schema defining the shape of application state, accessed at runtime via session.state. Used for validation and extracting defaults.

initialState?:

Partial<z.infer<TState>>
Initial state values. Must conform to the schema if provided.

memory?:

MastraMemory
Memory configuration shared across all modes. Propagated to mode agents that don't have their own memory.

modes:

HarnessMode[]
Available agent modes. At least one mode is required. When a top-level `agent` is provided, each mode layers its own instructions and tool overrides on top of the shared agent.
HarnessMode

id:

string
Unique identifier for this mode (e.g., `"plan"`, `"build"`).

name?:

string
Human-readable name for display.

default?:

boolean
Whether this is the default mode when the harness starts. Deprecated in favor of `metadata.default` or the top-level `defaultModeId`.

defaultModelId?:

string
Default model ID for this mode (e.g., `"anthropic/claude-sonnet-4-20250514"`). Used when no per-mode model has been explicitly selected.

description?:

string
Description surfaced in mode pickers and Studio UI.

instructions?:

string
Additional instructions layered above the backing agent's own instructions for this mode.

transitionsTo?:

string
Target mode ID to switch to on `submit_plan` approval. Must reference another mode's `id`.

metadata?:

Record<string, unknown>
Arbitrary metadata. `metadata.default === true` marks the default mode when `defaultModeId` is unset.

tools?:

ToolsInput
Replaces the backing agent's tools for this mode. Mutually exclusive with `additionalTools`.

additionalTools?:

ToolsInput
Tools layered on top of the backing agent's tools. Mutually exclusive with `tools`.

agent?:

Agent
The agent for this mode. Deprecated in favor of the top-level `agent` config with mode-level overrides.

agent?:

Agent
Shared backing agent that each mode forks and decorates. When provided, modes layer instructions and tool overrides on top of this agent instead of providing their own.

defaultModeId?:

string
Default mode to enter when a thread has no persisted mode. Takes precedence over `metadata.default` on individual modes.

instructions?:

string
Base instructions shared across all modes. Appended to the backing agent's instructions.

tools?:

ToolsInput | ((ctx) => ToolsInput)
Tools available to all agents across all modes. It can be a static tools object or a dynamic function that receives the request context.

workspace?:

Workspace | WorkspaceConfig | ((ctx) => Workspace)
Workspace configuration. Accepts a pre-constructed Workspace, a WorkspaceConfig for the harness to construct internally, or a dynamic factory function.

browser?:

MastraBrowser | ((ctx) => MastraBrowser)
Browser automation configuration. Propagated to mode agents that don't have their own browser configured.

subagents?:

HarnessSubagent[]
Subagent definitions. When provided, the harness creates a built-in `subagent` tool that parent agents can call to spawn focused subagents.
HarnessSubagent

id:

string
Unique identifier for this subagent type (e.g., `"explore"`, `"execute"`).

name:

string
Human-readable name shown in tool output.

description:

string
Description of what this subagent does. Used in the auto-generated tool description.

instructions:

string
System prompt for this subagent.

tools?:

ToolsInput
Tools this subagent has direct access to.

allowedHarnessTools?:

string[]
Tool IDs from the harness's shared `tools` config. Merged with `tools` above to let subagents use a subset of harness tools.

allowedWorkspaceTools?:

string[]
Workspace tool names the subagent is allowed to use. Uses the exposed names (after any renames via workspace tool config). When set, workspace tools not in this list are hidden from the model. Non-workspace tools are never affected. When omitted, all workspace tools are visible.

defaultModelId?:

string
Default model ID for this subagent type.

maxSteps?:

number
Optional maximum number of steps for the spawned subagent. Defaults to `50` when omitted.

stopWhen?:

LoopOptions['stopWhen']
Optional stop condition for the spawned subagent.

forked?:

boolean
When `true`, calls to this subagent default to forked mode: the subagent runs on a clone of the parent thread, reusing the parent agent’s instructions, tools, and model so the prompt-cache prefix stays intact. Requires `memory` to be configured. The subagent definition’s own `instructions`, `tools`, `allowedHarnessTools`, `allowedWorkspaceTools`, `defaultModelId`, `maxSteps`, and `stopWhen` are ignored in forked mode. Callers can still override per-invocation via `forked: false` in the `subagent` tool input. See the [Forked subagents](#forked-subagents) section below for full semantics.

resolveModel?:

(modelId: string) => MastraLanguageModel
Converts a model ID string (e.g., `"anthropic/claude-sonnet-4"`) to a language model instance. Used by subagents and observational memory model resolution.

omConfig?:

HarnessOMConfig
Default configuration for observational memory (observer/reflector model IDs and thresholds).

disableBuiltinTools?:

BuiltinToolId[]
Built-in harness tool IDs to remove from the `harnessBuiltIn` toolset. Valid values are `ask_user`, `submit_plan`, `task_write`, `task_update`, `task_complete`, `task_check`, and `subagent`.

heartbeatHandlers?:

HeartbeatHandler[]
Periodic background tasks started during `init()`. Use for gateway sync, cache refresh, and similar tasks.

idGenerator?:

() => string
= timestamp + random string
Custom ID generator for Harness-managed IDs such as threads and mode-run identifiers.

modelAuthChecker?:

ModelAuthChecker
Custom auth checker for model providers. Return `true`/`false` to override the default environment variable check, or `undefined` to fall back to defaults.

modelUseCountProvider?:

ModelUseCountProvider
Provides per-model use counts for sorting and display in `listAvailableModels()`.

modelUseCountTracker?:

ModelUseCountTracker
Callback invoked when a model is selected via `session.model.switch()`. Use to track and persist model usage for ranking.

customModelCatalogProvider?:

CustomModelCatalogProvider
Catalog hook for additional models (e.g., user-defined custom providers). Returned entries are merged into `listAvailableModels()`.

gateways?:

MastraModelGatewayInterface[]
Model gateways registered on the internal Mastra instance. Apps that need gateway-backed model resolution should also provide `resolveModel`.

toolCategoryResolver?:

(toolName: string) => ToolCategory | null
Maps tool names to permission categories (`'read'`, `'edit'`, `'execute'`, `'mcp'`, `'other'`). Used by the permission system to resolve category-level policies.

pubsub?:

PubSub
PubSub instance used by the internal Mastra instance and mode agents for event coordination.

threadLock?:

{ acquire, release }
Thread locking callbacks to prevent concurrent access from multiple processes. `acquire` should throw if the lock is held.

observability?:

ObservabilityEntrypoint
Observability entrypoint for tracing, scoring, and feedback. When provided, the internal Mastra instance produces trace spans for agent runs.

Properties
Direct link to Properties

id:

string
Harness identifier, set at construction.

session:

Session
The per-conversation state for the active conversation: identity, the active thread binding and reads, mode and model selection, run and abort state, the live stream, tool suspensions, follow-ups, approvals, permission grants, token usage, and the display-state snapshot. See the Session reference for the full API.

Methods
Direct link to Methods

Lifecycle
Direct 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()

Modes
Direct 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().

Models
Direct 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 }]

Threads
Direct 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 }

Messages
Direct 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.

Memory
Direct 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 control
Direct 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 approvals
Direct 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 plans
Direct 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 plan
Direct 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' },
})

Permissions
Direct 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'

Workspace
Direct 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 Memory
Direct 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 subagents
Direct 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 mode
Direct 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 constraints
Direct link to Semantics and constraints

  • Memory required. Forked mode calls memory.cloneThread to create the fork, so the harness must have memory configured 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, and stopWhen apply. The subagent definition's instructions, tools, allowedHarnessTools, allowedWorkspaceTools, defaultModelId, maxSteps, and stopWhen are 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 the subagent tool 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. The subagent entry is kept on the model side but its execute is 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 === true and metadata.parentThreadId === <parent>. By default, session.thread.list() hides these so they don't show up in user-facing thread pickers / startup flows. Pass includeForkedSubagents: true to 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 the flushMessages callback on AgentToolExecutionContext before 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 mode
Direct 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.

Events
Direct 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()

Events
Direct link to Events

The harness emits events through registered listeners. The following table lists the available event types:

Event typeDescription
mode_changedThe active mode changed.
model_changedThe active model changed.
thread_changedThe active thread changed.
thread_createdA new thread was created.
thread_deletedA thread was deleted.
state_changedHarness state was updated.
agent_startThe agent started processing.
agent_endThe agent finished processing.
message_startA new message started streaming.
message_updateA message was updated with new content.
message_endA message finished streaming.
tool_startA tool call started.
tool_approval_requiredA tool call requires user approval.
tool_suspendedA 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_updateA tool call was updated with progress.
tool_endA tool call finished.
tool_input_startTool input started streaming.
tool_input_deltaTool input received a streaming delta.
tool_input_endTool input finished streaming.
usage_updateToken usage was updated.
errorAn error occurred.
infoAn informational message was emitted.
system_reminderA system reminder was injected into the conversation.
state_signalA state signal was emitted (state-driven prompt injection).
reactive_signalA reactive signal fired in response to a state change.
notificationA notification was delivered to the thread inbox.
notification_summaryA summary of pending notifications was emitted.
follow_up_queuedA follow-up message was queued.
workspace_status_changedThe workspace status changed.
workspace_readyThe workspace finished initializing.
workspace_errorThe workspace encountered an error.
om_statusObservational Memory status update.
om_activationObservational Memory was activated for the thread.
om_model_changedAn Observational Memory role model changed (observer or reflector).
om_observation_startAn observation started.
om_observation_endAn observation completed.
om_observation_failedAn observation failed.
om_reflection_startA reflection started.
om_reflection_endA reflection completed.
om_reflection_failedA reflection failed.
om_buffering_startObservational Memory started buffering messages.
om_buffering_endObservational Memory finished buffering messages.
om_buffering_failedObservational Memory buffering failed.
om_thread_title_updatedObservational Memory updated the thread title.
subagent_startA subagent started processing.
subagent_text_deltaA subagent emitted a text delta.
subagent_tool_startA subagent started a tool call.
subagent_tool_endA subagent finished a tool call.
subagent_endA subagent finished processing.
subagent_model_changedA subagent's model changed.
task_updatedA task list was updated.
goal_evaluationA goal evaluation completed (when native agent goals are configured).
shell_outputA tool emitted shell output (stdout or stderr).
display_state_changedThe 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 tools
Direct link to Built-in tools

The harness provides built-in tools to agents in every mode:

ToolDescription
ask_userAsk the user a question and wait for their response. Supports free text, single-select choices, and multi-select choices.
submit_planSubmit a plan for user review and approval.
task_writeCreate or replace a structured task list for tracking progress. Assigns task IDs when omitted and returns the structured task list snapshot.
task_updateUpdate one tracked task by ID and return the structured task list snapshot.
task_completeMark one tracked task completed by ID and return the structured task list snapshot.
task_checkCheck the completion status of the current task list and return tasks, summary, incompleteTasks, and isError fields.
subagentSpawn 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 selections
Direct 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'],
})
}
}
})