Skip to content

Releases: mastra-ai/mastra

July 15, 2026

Choose a tag to compare

@PaulieScanlon PaulieScanlon released this 15 Jul 13:33

Highlights

Durable Agent Crash Recovery (server + client + boot-time)

Durable agents can now discover and recover orphaned RUNNING runs after a restart via DurableAgent.listActiveRuns(), recover(), and recoverActiveRuns(), plus Mastra.recoverAllDurableAgents() and an opt-in MastraConfig.recovery: { durableAgents: 'auto' } for boot-time recovery. There’s also a standard HTTP/client surface (POST /agents/:agentId/recover + client-js agent.recover({ runId })) to reattach and stream the remaining output from dashboards or admin tools.

Scoped AgentController Sessions (parallel sessions per resource)

AgentController sessions now support a scope/sessionScope, allowing multiple independent sessions to run in parallel over the same resourceId (e.g., one per git worktree) without sharing run loops, threads, or state. The scope is also exposed in request context for integrations, and controller APIs now report run activity (running + per-thread state) for better UIs.

New Workspace + Sandbox Providers for Mastra Platform

A new package, @mastra/platform-workspace@0.1.0, adds PlatformFilesystem and PlatformSandbox providers that connect agents to Mastra Platform sandboxes and bucket-backed filesystems via the workspace-proxy API. This enables running workspace-dependent tooling against Platform-managed infrastructure with env-var or config-based setup.

Mastra Code SDK Now Public + More Extensible Integrations

@mastra/code-sdk@0.1.0 is now published as a first-class package (previously internal), enabling third parties to build custom UIs/surfaces on top of the Mastra Code coding agent. It also adds async per-request extraTools resolution and a postToolObserver hook for reacting to tool calls without replacing built-ins.

MCP Server: Better Serverless Streaming + Dynamic Tooling + Notifications

@mastra/mcp adds serverlessStreaming to MCPServer.startHTTP() to deliver request-scoped SSE progress notifications even in serverless mode, and expands notification support (tools list changed, server logs, per-session resource subscriptions, and reliable broadcast across streamable HTTP). MCP tool execution context also gains optional log/progress functions so tools can report status back to clients.

Breaking Changes

  • None noted in this changelog.

Changelog

@mastra/core@1.51.0

Minor Changes

  • Added session scoping to AgentController so independent sessions can run in parallel over the same resource (for example one session per git worktree). (#19357)

    Previously createSession() was get-or-create by resourceId alone, so two callers sharing a resource always resolved to the same session — with one run loop, one thread binding, and shared mode/model/state. Passing the new scope option creates an independent session per scope:

    // Two independent sessions over the same resource:
    const a = await controller.createSession({
      resourceId: 'repo-123',
      scope: '/worktrees/feature-a',
      tags: { projectPath: '/worktrees/feature-a' },
    });
    const b = await controller.createSession({
      resourceId: 'repo-123',
      scope: '/worktrees/feature-b',
      tags: { projectPath: '/worktrees/feature-b' },
    });
    
    // Look up a scoped session later:
    const session = await controller.getSessionByResource('repo-123', '/worktrees/feature-a');

    Calls with the same resourceId and scope still resume the same session (get-or-create), and unscoped sessions behave exactly as before.

  • Added support for custom processors on createScorer judges. You can now pass inputProcessors, outputProcessors, errorProcessors, and maxProcessorRetries in a scorer's judge config (or per-step judge config) to apply processors to the internal judge agent — for example wiring up StreamErrorRetryProcessor to retry transient LLM errors while scoring. (#19195)

    import { createScorer } from '@mastra/core/evals';
    import { StreamErrorRetryProcessor } from '@mastra/core/processors';
    
    const scorer = createScorer({
      id: 'my-scorer',
      description: 'Scores responses',
      judge: {
        model: myModel,
        instructions: 'You are an expert evaluator...',
        errorProcessors: [new StreamErrorRetryProcessor()],
        maxProcessorRetries: 3,
      },
    });
  • Added support for resolving foreach concurrency at execution time. concurrency can now be a function that receives the foreach input and the workflow's init data and returns a number, in addition to a static number: (#19329)

    workflow.foreach(step, {
      concurrency: ({ inputData, getInitData }) => (getInitData().fast ? 10 : 1),
    });

    Durable agents use this to honor toolCallConcurrency: parallel tool calls now run concurrently (default 10) instead of always sequentially, while runs that require tool approval or use tools that can suspend still execute tool calls one at a time.

  • Added an option to retry unknown stream errors while allowing known authorization failures to surface immediately, and enabled resilient retries for coding agents. (#19290)

    import { StreamErrorRetryProcessor } from '@mastra/core/processors';
    
    const processor = new StreamErrorRetryProcessor({
      retryUnknownErrors: true,
      maxRetries: 2,
      delayMs: 3000,
    });
  • Added: runEvals() now supports gate-only runs. scorers is optional when at least one gate is provided. (#19348)

  • Added maxRetryAfterMs to StreamErrorRetryProcessor, with a default of 30 seconds, so provider Retry-After waits can't exceed a configured limit. Aborting during the wait now stops the processor retry before another model request. (#19383)

    Improved structured-output recovery so transport and provider failures don't trigger JSON prompt injection. Scorer judges that use Mastra's current generation API can use existing error processors for a coordinated retry budget. Legacy model adapters keep their separate generateLegacy() retry settings.

    Before

    import { StreamErrorRetryProcessor } from '@mastra/core/processors';
    
    new StreamErrorRetryProcessor({
      maxRetries: 2,
    });

    After

    import { StreamErrorRetryProcessor } from '@mastra/core/processors';
    
    new StreamErrorRetryProcessor({
      maxRetries: 2,
      maxRetryAfterMs: 30_000,
    });
  • Workflow run event topics are now cleaned up automatically. The evented workflow engine deletes each run's workflow.events.v2.<runId> pub/sub topic shortly after the run reaches a terminal state (success, failure, or cancellation), so persistent transports like Redis Streams no longer accumulate streams from finished workflow runs (#19123). (#19418)

    clearTopic is now part of the PubSub base class with a default no-op implementation. Custom transports that retain messages per topic should override it to delete that state:

    import { PubSub } from '@mastra/core/events';
    
    class CustomPubSub extends PubSub {
      async clearTopic(topic: string): Promise<void> {
        // delete retained state for the topic
      }
    }

    Callers no longer need to probe for the method before calling it — CachingPubSub and the durable-agent runtime now forward clearTopic unconditionally.

  • Added anonymous feature usage telemetry for server startup surface counts and a trackFeatureUsage() API. (#19159)

  • Added atomic caller-defined dataset IDs for idempotent dataset creation across built-in storage adapters. Supplying id to mastra.datasets.create() now creates the dataset once and resolves compatible retries to the persisted record; incompatible immutable identity fields throw DATASET_ID_CONFLICT. (#19370)

  • Added PROVIDER_TOOL_CALL observability spans for provider-executed tools (e.g. Anthropic code execution, server-side web search). Provider tool input and output are now visible in traces and Studio, with spans anchored to the AGENT_RUN parent. (#19261)

  • Added the ability to explicitly disable a storage domain on MastraCompositeStore by setting it to false in the domains config. A disabled domain no longer falls back to the editor or default store, so writes for that domain are dropped instead of silently landing in the fallback database. (#19059)

    const storage = new MastraCompositeStore({
      id: 'my-storage',
      default: libsqlStore,
      domains: {
        // don't persist traces/metrics when observability is turned off
        observability: false,
      },
    });

    prune() also accepts a per-call retention option that replaces the configured retention policies for that call only — for example to skip a domain (keep chat history) or prune more aggressively without reconstructing the store:

    // prune everything except the memory domain, one time
    await storage.prune({
      retention: {
        observability: { spans: { maxAge: '14d' } },
      },
    });
  • Added the authoritative session scope to agent controller request context for scoped session integrations. (#19446)

    const controllerContext = requestContext.get('controller');
    console.log...
Read more

July 6, 2026

Choose a tag to compare

@PaulieScanlon PaulieScanlon released this 08 Jul 15:12

Highlights

LiveKit Realtime Voice Agents (@mastra/livekit)

New @mastra/livekit package turns Mastra agents (or per-turn workflows) into realtime voice agents with LiveKit handling the full audio loop (WebRTC, VAD, STT/TTS, turn detection, barge-in) while Mastra drives replies, tools, and memory—plus built-in tracing and a Studio “voice mode”.

File-System Routed Mastra Primitives + Zero-Boilerplate Startup

Mastra can now auto-discover and register storage.ts, observability.ts, server.ts, studio.ts, workflows/*.ts, and agent processors from the src/mastra directory during mastra dev/mastra build; @mastra/deployer can also auto-construct a Mastra instance when src/mastra/index.ts is missing, enabling fully file-based projects with no new Mastra(...) entry file.

Unified Schedules API (Heartbeats → Schedules)

Agent “heartbeats” and workflow schedules are unified under mastra.schedules and /api/schedules (client + server), with deprecated/removed heartbeat routes and methods; persisted legacy rows are normalized (target.type: 'heartbeat''agent') so existing schedules keep firing across supported stores.

Workspace Provider Registry in MastraEditor

MastraEditor now supports a workspace-level provider registry: register a single createWorkspace factory that builds complete Workspace instances (filesystem + sandbox), and let stored agents reference them via { type: 'provider', provider, config } for cleaner hydration and multi-environment workspace setups.

Observability Improvements for Serverless + File-Based Config

Observability can now be file-routed via src/mastra/observability.ts, and ObservabilityEntrypoint adds flush() so mastra.observability.flush() works directly in serverless environments (delegating to all instances, similar to shutdown()).

Breaking Changes

  • Heartbeats were renamed to schedules: mastra.heartbeatsmastra.schedules, config heartbeatschedules, type renames (e.g., HeartbeatAgentSchedule), new agent schedule IDs use agent_, and the default fire signal tag is now <schedule>.
  • Server removed /api/heartbeats/* routes in favor of the unified /api/schedules/* API.
  • Client deprecated *Heartbeat() methods in favor of unified schedule methods (to be removed in a future release).

Changelog

@mastra/core@1.50.0

Minor Changes

  • Added file-system-routed observability singleton. Place an observability.ts file in your mastra directory that default-exports an ObservabilityEntrypoint, and it will be auto-discovered and registered when running mastra dev or mastra build. Code-registered observability takes precedence if both are present. (#18887)

    // src/mastra/observability.ts
    import { Observability, MastraStorageExporter } from '@mastra/observability';
    
    export default new Observability({
      configs: { default: { serviceName: 'mastra', exporters: [new MastraStorageExporter()] } },
    });
  • Added file-system routed storage support. A storage.ts file under the mastra directory is now auto-discovered and registered during mastra dev / mastra build. The default export replaces the InMemoryStore fallback. Code-registered storage (passed to new Mastra({storage})) wins on collision. (#18885)

    // src/mastra/storage.ts
    import { LibSQLStore } from '@mastra/libsql';
    
    export default new LibSQLStore({ url: 'file:local.db' });
  • Added file-system-routed workflows support. Workflows placed in workflows/*.ts under the mastra directory are now auto-discovered and registered during mastra dev / mastra build, matching the existing file-based agents convention. Code-registered workflows win on name collisions. (#18883)

    // src/mastra/workflows/onboarding.ts
    import { createWorkflow } from '@mastra/core/workflows';
    
    export default createWorkflow({ id: 'onboarding' /* ...steps */ });
  • Added workspace-level provider registry to MastraEditor. You can now register WorkspaceProvider factories that build complete Workspace instances as a single unit, instead of composing from separate filesystem and sandbox providers. Stored agents can reference a workspace provider via { type: 'provider', provider: 'my-cloud', config: { ... } } and the editor will call the registered factory during agent hydration. (#18781)

    import { MastraEditor } from '@mastra/editor';
    import { Workspace } from '@mastra/core/workspace';
    
    const editor = new MastraEditor({
      workspaces: {
        'my-cloud': {
          id: 'my-cloud',
          name: 'My Cloud Workspace',
          createWorkspace: config =>
            new Workspace({
              id: 'cloud-ws',
              name: 'Cloud WS',
              filesystem: new MyCloudFilesystem(config),
              sandbox: new MyCloudSandbox(config),
            }),
        },
      },
    });
    
    // Stored agent workspace reference using the provider:
    // { type: 'provider', provider: 'my-cloud', config: { region: 'us-east-1' } }
  • models.dev gateway: honor per-model provider overrides (endpoint, request shape, SDK). (10959d5)

    A provider can now serve individual models over a different base URL / request shape than the provider default — e.g. a model served over the OpenAI Responses API while the provider default is chat-completions. The models.dev gateway now reads each model's provider block (api, shape, npm), so resolveLanguageModel routes shape: "responses" models via the OpenAI Responses API and buildUrl prefers a per-model api when present. Providers without per-model overrides are unaffected.

  • Added file-system routed server config singleton. Place a server.ts file in your mastra directory that default-exports a ServerConfig object, and it will be auto-discovered and registered when running mastra dev or mastra build. Code-registered server config takes precedence if both are present. (#18888)

  • Added file-system-routed agent processors. Place input and output processor files under agents/<name>/processors/input/ and agents/<name>/processors/output/. Each file default-exports a processor, and they are auto-discovered and merged with config-defined processors when running mastra dev or mastra build. Config-defined processors run first, and a dynamic (function) inputProcessors/outputProcessors in config.ts takes precedence over discovered files. (#18890)

    src/mastra/agents/support/
    ├── config.ts
    ├── instructions.md
    └── processors/
        ├── input/
        │   └── moderation.ts
        └── output/
            └── redact-pii.ts
    
    // src/mastra/agents/support/processors/input/moderation.ts
    import { ModerationProcessor } from '@mastra/core/processors';
    
    export default new ModerationProcessor({ model: 'openai/gpt-5-nano' });
  • Renamed heartbeats to schedules. Agent heartbeats and workflow schedules are now one unified Schedules API: mastra.schedules manages both. The name "heartbeat" implied a liveness check; these are cron-based agent schedules, so they are now simply called schedules. (#18874)

    Before

    const hb = await mastra.heartbeats.create({
      agentId: 'chef',
      cron: '0 9 * * *',
      prompt: 'Suggest a dish of the day',
    });
    
    await mastra.heartbeats.pause(hb.id);

    After

    // Schedule an agent (was a heartbeat)
    const schedule = await mastra.schedules.create({
      agentId: 'chef',
      cron: '0 9 * * *',
      prompt: 'Suggest a dish of the day',
    });
    
    // Schedule a workflow with the same API
    await mastra.schedules.create({
      workflowId: 'daily-report',
      cron: '0 6 * * *',
      inputData: { region: 'us' },
    });
    
    await mastra.schedules.pause(schedule.id);

    What changed:

    • mastra.heartbeats is now mastra.schedules and also creates, lists, updates, pauses, resumes, runs, and deletes workflow schedules. Results are discriminated by agentId vs workflowId.
    • The Mastra config option heartbeat: { ... } (lifecycle hooks) is now schedules: { ... }, and hook types were renamed (HeartbeatHooksScheduleHooks, HeartbeatPrepareContextSchedulePrepareContext, and so on).
    • New agent schedule ids use the agent_ prefix instead of hb_. Existing hb_ ids keep working.
    • The default signal tag an agent receives on a fire is now <schedule> instead of <heartbeat>.
    • Types renamed: HeartbeatAgentSchedule, CreateHeartbeatInputCreateAgentScheduleInput, HeartbeatScheduleTargetAgentScheduleTarget (persisted target.type is now 'agent' instead of 'heartbeat').

    Existing schedules stored in your database keep working: rows persisted with the old target.type: 'heartbeat' are read as 'agent' automatically and keep firing.

  • Added file-system routed studio config singleton. Place a studio.ts file in your mastra directory that default-exports a StudioConfig object, and it will be auto-discovered and registered when running mastra dev or mastra build. Code-registered studio config takes precedence if both are present. (#18889)

Patch Changes

  • Fix workflow snapshot bloat on agent HITL tool-approval suspensions (#18647). Agent-run snapshots previously grew with thread length × number of ...
Read more

July 3, 2026

Choose a tag to compare

@PaulieScanlon PaulieScanlon released this 07 Jul 07:59

Highlights

Opt-in Storage Retention + storage.prune() (Core + Postgres/libSQL/MongoDB)

Mastra now supports per-domain/per-table retention policies (retention: { ...maxAge }) and a safe, batched, resumable storage.prune() to delete aged data across major “growth” tables (memory, observability spans, workflow snapshots, background tasks, experiments, schedules, etc.). Postgres/libSQL/MongoDB adapters add first-class pruning implementations (including partition/chunk drops for v-next observability in Postgres).

Multi-tenant Isolation for Datasets, Experiments, Scores, and Scorers

End-to-end optional organizationId/projectId scoping was added across dataset and experiment reads/deletes (including storage-layer predicates to prevent cross-tenant access), plus tenancy metadata for persisted scores and stored scorer definitions with scoped list APIs. Server routes and client-js methods also accept tenancy parameters so HTTP and SDK usage can enforce the same isolation.

Persisted Trace Scoring APIs (scoreTrace, scoreTraceBatch) + Score Provenance

New scoreTrace() and scoreTraceBatch() let you score already-stored traces without re-running agents, using an existing scorer instance and bounded concurrency for batches. Persisted scores now optionally include batchId, datasetId, and datasetItemId so baseline scoring passes can be grouped and joined back to dataset items across supported stores.

Agent Authoring & Runtime Improvements (Nested Subagents, Durable Parity, Model Router Safety)

File-based agents can now nest subagents/ up to three levels deep, enabling deeper delegation hierarchies without custom wiring. Durable agents gained significant behavior/streaming parity fixes (signals draining/echo, stop semantics, processor/tool lifecycle hooks, header sanitization, replay ordering), and the model router now strips unsupported sampling params (e.g., temperature) to prevent 400s on models that don’t accept them.

New Workspace/Sandbox Options and Providers (Apple Container, Mesa, Railway, E2B)

New @mastra/apple-container adds an Apple container CLI workspace sandbox provider, and @mastra/mesa adds a Mesa filesystem provider for workspaces. RailwaySandbox gains checkpoint-backed restart/reconnect, and E2BSandbox adds a forwarded network option for finer-grained network controls and per-host request transforms.

Breaking Changes

  • @mastra/playground-ui removed root named exports; import from explicit subpaths (e.g. @mastra/playground-ui/components/Button).
  • @mastra/playground-ui removed Searchbar; use InputGroup composition instead.

Changelog

@mastra/core@1.49.0

Minor Changes

  • Added opt-in storage retention. Declare per-table maxAge policies in the retention config, then call storage.prune() to delete rows older than their age. Anything you don't configure is kept forever, so there is no change until you opt in. (#18733)

    Retention covers growth tables across ten domains — memory (threads, messages, resources), threadState, observability (spans), scores, workflows (run snapshots), backgroundTasks, experiments, notifications, harness (sessions), and schedules (fire history). Anchors are chosen so maxAge is honest: creation time for append-only logs, last activity for workflow snapshots and thread state, and completion time for background tasks and experiments (in-flight work is never pruned). User-authored artifacts and config (agents, skills, workspaces, datasets, schedule definitions, and so on) are not prunable.

    prune() is safe on large tables: it deletes in bounded, batched, resumable, cancellable chunks and never locks the database for long. Call it from your own scheduler; when a result reports done: false, eligible rows remain and the next run continues. prune() only deletes rows — reclaiming disk to the OS is left to the underlying database and the operator.

    const storage = new MastraCompositeStore({
      id: 'composite',
      retention: {
        memory: { messages: { maxAge: '30d' }, threads: { maxAge: '90d' } },
        observability: { spans: { maxAge: '7d' } },
      },
      domains: {
        /* ... */
      },
    });
    
    // Wire this to your own cron — Mastra never runs it for you.
    const results = await storage.prune();
  • Added maxDurationMs, maxWidth, and maxHeight options to BrowserRecordingOptions. These can now be set on the recording config object to provide defaults for every recording, instead of relying on agent instructions to pass them to the tool at start time. (#18814)

    const browser = new AgentBrowser({
      recording: {
        outputDir: './recordings',
        maxDurationMs: 60_000,
        maxWidth: 1280,
        maxHeight: 720,
      },
    });

    Per-recording overrides via the browser_record tool still take precedence.

  • File-based agents can now nest subagents up to three levels deep. A subagent directory can declare its own subagents/, and each level is assembled and wired into its parent as a delegation tool. Levels deeper than the cap are ignored with a warning. (#18780)

    src/mastra/agents/
      supervisor/            # depth 0
        subagents/
          researcher/        # depth 1
            subagents/
              summarizer/    # depth 2
    
  • Fixed a cross-tenant data-access issue on datasets by scoping DatasetsManager.get and DatasetsManager.delete to tenancy filters. (#18750)

    Previously get({ id }) and delete({ id }) looked up a dataset by its primary key alone. Any caller who knew a dataset id could read or delete it regardless of which organizationId / projectId it belonged to. This is now closed at the storage layer via a scoped SQL predicate (option (a) — no fetch-then-assert).

    What changed

    • DatasetsManager.get and DatasetsManager.delete accept optional organizationId and projectId.
    • The tenancy is stashed on the returned Dataset handle and forwarded to every downstream storage call (getDetails, update, addItem, item batch ops, startExperimentAsync).
    • The abstract storage contract (getDatasetById, deleteDataset) gained an optional filters?: DatasetTenancyFilters arg.
    • Item-mutation inputs (AddDatasetItemInput, UpdateDatasetItemInput, BatchInsertItemsInput, BatchDeleteItemsInput) and UpdateDatasetInput accept optional filters for the internal existence check.

    Behavior

    • Omitting tenancy preserves the existing behavior (no predicate added) — fully backwards compatible.
    • On tenancy mismatch, get throws NOT_FOUND (returns null at the storage layer) and delete is a silent no-op — matching how a missing id already behaves, so existence does not leak through error timing or messages.

    Example

    // Before
    const ds = await mastra.datasets.get({ id });
    await mastra.datasets.delete({ id });
    
    // After — scope to a tenant
    const ds = await mastra.datasets.get({ id, organizationId, projectId });
    await mastra.datasets.delete({ id, organizationId, projectId });
  • Added scoreTrace() and scoreTraceBatch() to @mastra/core/evals/scoreTraces for scoring stored traces without re-running the agent. (#18331)

    • scoreTrace() can score either a stored trace reference or a preloaded TraceRecord, and it returns the persisted ScoreRowData after the write.
    • scoreTraceBatch() runs one scorer instance across multiple stored traces with bounded concurrency and returns per-target success and failure results.

    Why

    This gives baseline-style callers a small public API for persisted trace scoring when they already have a scorer instance, without widening the existing workflow-based scoreTraces() API.

    Before

    await scoreTraces({
      mastra,
      scorerId: 'helpfulness',
      targets: [{ traceId, spanId }],
    });

    After

    import { scoreTrace, scoreTraceBatch } from '@mastra/core/evals/scoreTraces';
    
    const savedScore = await scoreTrace({
      storage,
      scorer,
      target: { trace: preloadedTrace, spanId },
      batchId,
      datasetId,
      datasetItemId,
    });
    
    const result = await scoreTraceBatch({
      storage,
      scorer,
      targets,
      batchId,
      datasetId,
    });
  • Add optional batchId, datasetId, and datasetItemId fields to persisted scores so saved baseline scores can be grouped as one scoring pass and joined back to the dataset items they came from. (#18331)

    • scoreTrace() accepts top-level batchId, datasetId, and datasetItemId when persisting a score for a stored trace.
    • ScoreRowData and score save payloads now include nullable batchId, datasetId, and datasetItemId.
    • Built-in stores with explicit score schema or attribute mappings now persist these provenance fields on saved scores.
    • D1, DSQL, MSSQL, and Upstash score stores now apply additive provenance migrations or deterministic score ordering for persisted score reads.
    await scoreTrace({
      storage,
      scorer,
      target: { traceId },
      batchId: 'baseline-batch-1',
      datasetId,
      datasetItemId,
    });
  • Added multi-tenant scoping to stored scorer definitions. Stored scorers now persist optional organizationId and projectId on the definition record, and list/listResolved accept matching filters to scope results by tenant. The Postgres adapter backfills the new columns and applies the scoped f...

Read more

July 1, 2026

Choose a tag to compare

@PaulieScanlon PaulieScanlon released this 01 Jul 15:35

Highlights

Heartbeats (scheduled agents on cron)

New mastra.heartbeats APIs let you schedule agents to run on a recurring cron, either delivering into an existing thread as a normal signal or running threadless; schedules persist across restarts and are available in @mastra/core, @mastra/server (/api/heartbeats), and @mastra/client-js.

File-based agents & subagents (FS convention + bundler auto-registration)

You can now define agents by dropping directories under src/mastra/agents/<name>/ (with config.ts/instructions.md, tools/, skills/, optional memory.ts and workspace/ seeding), and the deployer/build pipeline will bundle + register them automatically; supports one-level subagents/<childId>/ that become delegatable tools.

Human-in-the-loop reliability: storage-backed suspended run discovery

New agent.listSuspendedRuns() reads pending approvals/suspends from storage (not memory), enabling approval UIs to recover after refresh/restart and across multiple server instances; sendToolApproval() now falls back to this discovery when no in-memory active run is found.

Observational Memory upgrades: Extractor API + OM-managed working memory

@mastra/memory adds a public Observational Memory Extractor API (inline XML extraction + structured follow-ups with built-in extractors like task/suggested response/thread title) and an option for the Observer to automatically manage working memory via observationalMemory.observation.manageWorkingMemory.

Durable/Inngest execution parity & stronger auth context propagation

InngestAgent is brought to parity with DurableAgent (execution options, abort handling, idle-aware resume, generate()/resumeGenerate()), Inngest observe() now replays buffered events for late subscribers, and Inngest workflows now support the FGA actor signal end-to-end across durable/nested boundaries.

Breaking Changes

  • @mastra/vercel: sandbox exports renamed—VercelSandbox now means MicroVM-backed Vercel Sandbox; the serverless implementation is now VercelServerlessSandbox (and VercelMicroVMSandbox is replaced by VercelSandbox).

Changelog

@mastra/core@1.48.0

Minor Changes

  • Renamed the AgentController interval API. heartbeatHandlers is now intervalHandlers, the HeartbeatHandler type is now IntervalHandler, and the removeHeartbeat()/stopHeartbeats() methods are now removeInterval()/stopIntervals(). This better reflects that these are fixed-interval background tasks, not liveness pings, and is distinct from the unrelated mastra.heartbeats scheduled-agent feature. (#18665)

    Before

    const { controller } = await createMastraCode({
      heartbeatHandlers: [{ id: 'sync', intervalMs: 60_000, handler: async () => {} }],
    });
    await controller.removeHeartbeat({ id: 'sync' });
    await controller.stopHeartbeats();

    After

    const { controller } = await createMastraCode({
      intervalHandlers: [{ id: 'sync', intervalMs: 60_000, handler: async () => {} }],
    });
    await controller.removeInterval({ id: 'sync' });
    await controller.stopIntervals();
  • Added optional maxSteps to ScorerJudgeConfig and GoalConfig, letting callers control the internal judge agent's agentic-loop iteration limit instead of relying on the implicit default of 5. Also fixed judge agent not receiving Mastra registration, which caused the observer's API key resolution to fail silently. (#18544)

  • Added optional maxSteps to ScorerJudgeConfig, letting callers control the internal judge agent's agentic-loop iteration limit instead of relying on the implicit default of 5. (#18544)

  • Added createCodingAgent factory and a reusable buildBasePrompt so other projects can build a coding agent on top of the same defaults MastraCode uses. (#18695)

    The factory wires sensible, portable defaults that you can override per field:

    • Workspace — a local filesystem + sandbox rooted at process.cwd() (set basePath, pass your own workspace, or pass workspace: undefined to opt out entirely).
    • Task signalsTaskSignalProvider so a task list persists across turns.
    • Error handling — retries on ECONNRESET and bad-request errors, plus prefill and provider-history compatibility processors.
    • Goal judging — the default goal judge prompt.

    buildBasePrompt is parameterized with productName, coAuthorName (both default to "Mastra Code"), and coAuthorEmail (defaults to "noreply@mastra.ai"), so you can brand the system prompt and commit trailer without forking it.

    import { createCodingAgent } from '@mastra/core/coding-agent';
    
    const agent = createCodingAgent({
      id: 'my-coding-agent',
      name: 'My Coding Agent',
      model: 'openai/gpt-5',
      instructions: 'You help with my project.',
      tools: {},
      basePath: '/path/to/repo',
    });
  • Added heartbeats: schedule an agent to run on a recurring cron, either inside an existing conversation thread or on its own. (#18184)

    A heartbeat fires a prompt to an agent on a schedule. When it has a thread, the run is delivered into that thread as a normal agent signal, so anything watching the thread sees it like any other message; without a thread, the agent just runs in isolation. Each heartbeat has its own id and an optional name, so one agent or thread can have several heartbeats with different schedules and prompts. The id is generated for you, or you can pass your own id to create for a stable handle (it's normalized to hb_<slug>). Heartbeats are persisted, so they keep firing across process restarts with no extra setup.

    const hb = await mastra.heartbeats.create({
      agentId: 'chef',
      name: 'morning-checkin',
      threadId,
      resourceId,
      cron: '*/5 * * * *',
      prompt: 'Check in on the user',
      ifActive: { behavior: 'discard' }, // skip if the user is mid-conversation
      ifIdle: { behavior: 'wake' }, // wake the agent if the thread is idle
    });
    
    // Threadless: run the agent on a cron with no conversation.
    await mastra.heartbeats.create({
      agentId: 'chef',
      cron: '0 * * * *',
      prompt: 'Run the hourly summary',
    });
    
    await mastra.heartbeats.list({ agentId: 'chef' });
    await mastra.heartbeats.get(hb.id);
    await mastra.heartbeats.update(hb.id, { prompt: 'check in gently' });
    await mastra.heartbeats.pause(hb.id);
    await mastra.heartbeats.resume(hb.id);
    await mastra.heartbeats.run(hb.id); // fire once now
    await mastra.heartbeats.delete(hb.id);

    The same CRUD is available over HTTP through @mastra/server (under /api/heartbeats) and as top-level methods on the @mastra/client-js client (client.createHeartbeat, client.getHeartbeat, client.listHeartbeats, etc.).

    Lifecycle hooks

    React to heartbeat runs via heartbeat on the Mastra constructor. It's a single hook bundle that runs for every agent's heartbeats; each hook receives the firing agentId so you can branch on it. prepare resolves fire-time parameters (for example, creating a fresh thread per fire), and onFinish / onError / onAbort mirror agent.stream.

    new Mastra({
      // ...
      heartbeat: {
        // Return overrides, `null` to skip this fire, or `undefined` to use defaults.
        prepare: async ({ agentId, heartbeat }) => {
          if (agentId === 'chef' && heartbeat.name === 'daily-digest') {
            return { threadId: await createDailyThread(), resourceId: 'slack:U095PUH0FKL' };
          }
        },
        onFinish: ({ agentId, outcome, result, heartbeat }) => {
          metrics.record({ agentId, heartbeat: heartbeat.name, outcome });
        },
        onError: ({ agentId, error, phase, heartbeat }) => {
          alerts.send(`heartbeat ${agentId}/${heartbeat.name} failed in ${phase}: ${error.message}`);
        },
      },
    });

    Signal shaping

    A heartbeat fire surfaces to the agent as a signal. By default it uses the notification type and renders as <heartbeat>…</heartbeat>; override signalType and tagName to change either. ifActive and ifIdle mirror the agent.sendSignal options shape ({ behavior, attributes }, plus streamOptions on ifIdle) and stay JSON-serializable so they persist with the schedule. ifIdle.streamOptions currently accepts requestContext, which is rehydrated onto the woken run. Top-level attributes are rendered on the signal tag, and top-level providerOptions are merged into the signal payload on every fire.

    await mastra.heartbeats.create({
      agentId: 'chef',
      threadId,
      resourceId,
      cron: '*/5 * * * *',
      prompt: 'Check in on the user',
      tagName: 'check-in', // renders as <check-in>…</check-in>
      attributes: { source: 'cron' },
      providerOptions: { openai: { store: false } },
      ifIdle: {
        behavior: 'wake',
        streamOptions: { requestContext: { locale: 'en-US' } },
      },
    });
  • add OM-managed working memory (#18654)

    Adds observationalMemory.observation.manageWorkingMemory so the Observer can update working memory automatically instead of requiring the main agent to call the working memory tool.

    new Memory({
      options: {
        workingMemory: { enabled: true },
        observationalMemory: {
          enabled: true,
          observation: { manageWorkingMemory: true },
        },
      },
    });

    This option adds WorkingMemoryExtractor, defaults `workingMemory.ag...

Read more

June 26, 2026

Choose a tag to compare

@PaulieScanlon PaulieScanlon released this 01 Jul 08:45

Highlights

AI SDK v7 (LanguageModelV4) support + upgraded model router

Agents can now accept AI SDK v7 provider models directly (LanguageModelV4), while still supporting v4/v5/v6; Mastra auto-detects model spec versions so you can mix providers across agents. The model router was upgraded to resolve V4 models natively, and modelSettings adds a new reasoning effort control for V4-capable providers.

DurableAgent API parity (stream/resume/generate) + better cancellation & tracing

DurableAgent now mirrors Agent across stream/resume/generate, including full AgentExecutionOptions support (model settings, stop conditions, transforms, per-call instructions/system, tracing options, actor, etc.). New abort surfaces (abortSignal, result.abort(), onAbort) and forwarded abort to delegated subagents improve control, and durable runs now emit richer AGENT_RUN span metadata for observability.

Gateway-first model discovery & auth via new GatewayManager (Harness hooks removed)

Harness now sources model catalog, auth status, mode agents, observational-memory models, and subagents entirely from registered gateways (no fallback to resolveModel, customModelCatalogProvider, or modelAuthChecker, which were removed). Shared gateway operations are centralized in a new GatewayManager exported from @mastra/core, and gateways can implement handlesModel to claim bare provider model IDs for auth resolution.

AgentController becomes the canonical “Harness” surface across core/server/client

Harness is renamed to AgentController with a new canonical import path @mastra/core/agent-controller, and server routes are now exclusively under /agent-controller/.... Client usage follows suit (MastraClient.getAgentController, listAgentControllers, AgentControllerSession), with enriched session state (OM progress, token usage, behavior settings) and thread scoping via session tags (e.g. per git worktree).

Evals quality gates + micro-scorer checks namespace

runEvals now supports pass/fail gates, per-scorer { scorer, threshold } (including { min, max }), and returns verdict, gateResults, and thresholdResults. @mastra/evals adds checks micro-scorers (text assertions and tool-use assertions) to make common eval criteria easy to compose.

Breaking Changes

  • Harness→AgentController migration: @mastra/server removed legacy /harness/... routes and harness:* permissions; clients must use /agent-controller/... and agent-controller:* permissions.
  • @mastra/client-js removed deprecated getHarness/listHarnesses and Harness/HarnessSession classes (breaking for recent client users).
  • Harness config removed resolveModel, customModelCatalogProvider, and modelAuthChecker; register model gateways via gateways instead.
  • Session identity is now required: id and ownerId are mandatory on harness.createSession() and Session/SessionIdentity constructors (no defaults).
  • Workspace/browser ownership moved to per-session: Session constructor now requires workspace: Workspace; HarnessConfig.workspace no longer accepts WorkspaceConfig shorthand (must provide Workspace instance or factory).

Changelog

@mastra/core@1.47.0

Minor Changes

  • Added support for AI SDK v7 models (LanguageModelV4). You can now pass any AI SDK v7 provider model directly to an agent, alongside the existing AI SDK v4, v5, and v6 support. (#18477)

    import { Agent } from '@mastra/core/agent';
    import { openai } from '@ai-sdk/openai'; // AI SDK v7
    
    const agent = new Agent({
      name: 'my-agent',
      instructions: 'You are a helpful assistant.',
      model: openai('gpt-5'),
    });

    Mastra detects the model's specification version automatically, so mixing models from different AI SDK versions across your agents continues to work without any extra configuration.

  • Finished closing the gap between DurableAgent and Agent. After this change the durable agent surface mirrors the in-process agent's stream, resume, and generate APIs. (#18508)

    What's new

    • abortSignal on stream() and resume(), plus result.abort() on stream(), resume(), and observe(). result.abort() on streamUntilIdle() fans out to every inner run.
    • untilIdle on resume() (it already existed on stream()), using the shared runWithIdleWrapper so both paths drive the same idle loop.
    • DurableAgent.generate() and DurableAgent.resumeGenerate() wrap stream() / resume() and resolve a FullOutput even when the run suspends mid-flight.
    • delegation callbacks (onDelegationStart, onDelegationComplete, messageFilter) are forwarded to convertTools at prepare time and baked into sub-agent tool wrappers.
    • Per-call clientTools and toolsets survive in-process resume via the run registry (cross-process resume still falls back to the agent's static tools).
    • Scorers configured on the wrapped agent or passed per call now actually execute under durable runs and emit ON_SCORER_RUN payloads matching the non-durable shape.
    • AGENT_RUN spans now carry conversationId, instructions, resolvedVersionId, entityVersionId, and the agent's tracingPolicy. Resume spans use 'agent run: <id> (resumed)' and include resumedFromSpanId.

    Example

    import { createDurableAgent } from '@mastra/core/agent/durable';
    
    const durable = createDurableAgent({ agent: myAgent });
    
    // 1. generate() — drains a durable run to a single FullOutput
    const out = await durable.generate('Plan a week in Lisbon', {
      abortSignal: controller.signal,
      modelSettings: { temperature: 0.2 },
    });
    
    // 2. stream() with result.abort() — cancel mid-run
    const result = await durable.stream('Long research task');
    setTimeout(() => result.abort(), 5_000);
    for await (const chunk of result.output.fullStream) {
      process.stdout.write(chunk.payload?.text ?? '');
    }
    
    // 3. Suspend → resumeGenerate() round-trip (e.g. tool approval)
    const first = await durable.generate('Run the dangerous tool', {
      requireToolApproval: true,
    });
    if (first.finishReason === 'suspended') {
      const final = await durable.resumeGenerate(first.runId!, { approved: true });
      console.log(final.text);
    }
    
    // 4. resume({ untilIdle }) — drive a resume through to a quiescent state
    await durable.resume(runId, resumeData, { untilIdle: true });
  • Brought DurableAgent.stream() to parity with Agent.stream() for the full AgentExecutionOptions surface, so the same call works on both. (#18461)

    What's new

    DurableAgent.stream() now honors these options that were previously dropped or silently coerced:

    • Full modelSettings (was: only temperature) — maxOutputTokens, topP, topK, presencePenalty, frequencyPenalty, stopSequences, seed, headers
    • stopWhen, prepareStep, isTaskComplete, transform
    • Per-call instructions and system
    • disableBackgroundTasks, tracingOptions, actor
    • Function-form requireToolApproval(toolName, args, …) (was: coerced to "approve all" — now evaluated per tool call)
    • New callbacks: onAbort and onIterationComplete (joining the existing onChunk / onStepFinish / onFinish / onError / onSuspended bridge)

    Example

    The same options that work on Agent.stream() now work on DurableAgent.stream():

    const durable = createDurableAgent({ agent, pubsub });
    
    await durable.stream('Plan the trip', {
      modelSettings: { temperature: 0.2, maxOutputTokens: 500, topP: 0.9 },
      stopWhen: ({ steps }) => steps.length >= 3,
      prepareStep: ({ stepNumber }) => ({
        activeTools: stepNumber === 0 ? ['search'] : ['book'],
      }),
      requireToolApproval: ({ toolName }) => toolName === 'book',
      onIterationComplete: ({ iteration }) => console.log('iter', iteration),
    });
  • Resolve all Harness models through gateways. The Harness now builds its available-models catalog, model auth status, mode agents, Observational Memory models, and subagents from the gateways you register, instead of the separate resolveModel, customModelCatalogProvider, and modelAuthChecker config hooks. Removed those three options from HarnessConfig (and the ModelAuthChecker type) — register a gateway via gateways instead. (#18382)

    listAvailableModels() and getCurrentModelAuthStatus() are now sourced entirely from gateways: model discovery comes from each gateway's fetchProviders() (a network call for gateways like models.dev and Netlify), and auth status is resolved through the same gateway chain the model router uses at run time (resolveAuth(), falling back to getApiKey()). There is no static provider-registry fallback — everything the model picker shows comes from a gateway.

    The gateway-chain operations shared by ModelRouterLanguageModel and the Harness (gateway merging, model→gateway selection, auth resolution, provider/model listing) are now centralized in a new GatewayManager class exported from @mastra/core. Both consumers delegate to it, eliminating duplicated logic. defaultGateways is still re-exported from the same paths for backward compatibility.

  • Added reasoning option to modelSettings for controlling model reasoning effort level. This option accepts standardized levels ('provider-default' | 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh') and is effective with LanguageModelV4 (AI SDK v7) providers that support reasoning. When used with older model providers (V2/V3), the option is a no-op....

Read more

June 24, 2026

Choose a tag to compare

@PaulieScanlon PaulieScanlon released this 24 Jun 16:40
307573b

Highlights

Multi-session Harness architecture (Session-first APIs)

Harness is now a pure factory/shared-resource owner: create isolated sessions via harness.createSession() (get-or-create by resourceId), with run control, state, event bus, model/mode switching, permissions, OM, subagent settings, and thread lifecycle all moved onto Session domains (e.g. session.sendMessage(), session.thread.*, session.model.switch(), session.subscribe()), enabling safe concurrent multi-user/multi-thread hosting.

Drive Harness sessions over HTTP (and from the JS client)

Registered harnesses on Mastra can now be operated via new harness-scoped server routes (send/steer/abort/approve tool calls, manage threads, read state, and subscribe via SSE), and @mastra/client-js adds a first-class harness resource (client.getHarness(id).session(resourceId)) to control sessions remotely.

Cross-process signal delivery with distributed leasing + new accepted contract

Signal/message APIs now return an accepted promise resolving to a discriminated routing decision (wake/deliver/persist/discard) with authoritative runId when applicable, and introduce a LeaseProvider abstraction (in-memory and Redis Streams implementations) to ensure only one process owns/wakes a thread run in multi-instance/serverless deployments.

Deterministic agent experiments with item-level tool mocks + multi-tenant datasets/experiments

Dataset items can now declare ordered toolMocks (with strict/ignore arg matching) and experiments return a toolMockReport, enabling deterministic replay without running real tools; datasets and experiments also gain optional (organizationId, projectId) tenancy scoping plus dataset-level candidate identity (candidateKey, candidateId) and a new dataset item source type 'candidate-screener'.

New integrations & streaming ergonomics

New packages add major deployment/integration options: @mastra/next (Next.js App Router adapter), @mastra/tanstack-start (TanStack Start adapter), and @mastra/archil (Archil filesystem provider for workspaces); streaming gains a unified untilIdle option on DurableAgent.stream() and InngestAgent.stream() to keep streams open through background task continuations.

Breaking Changes

  • Harness no longer exposes a singleton harness.session; callers must use await harness.createSession() and operate on the returned Session.
  • Run control moved from Harness.* to Session (e.g. sendMessage, sendSignal, abort, respondToToolSuspension, etc. removed from Harness).
  • Event subscription moved from Harness.subscribe() to session.subscribe() (session-scoped event isolation).
  • Thread lifecycle APIs moved to session.thread.*; Harness no longer exposes createThread/switchThread/cloneThread/renameThread/detachFromCurrentThread or harness.memory.
  • Model/mode switching moved to session.model.switch() and session.mode.switch(); OM accessors moved to session.om.*; permissions to session.permissions.*; subagent model accessors to session.subagents.model.*.
  • Deprecated Harness.getState() / Harness.setState() compatibility wrappers removed (use session.state.get() / session.state.set()).

Changelog

@mastra/core@1.46.0

Minor Changes

  • Removed the deprecated Harness.getState() and Harness.setState() compatibility wrappers, along with the unused private updateState. Harness state has lived on the session for a while; these were thin proxies marked @deprecated. (#18200)

    Before

    const state = harness.getState();
    await harness.setState({ count: 1 });

    After

    const state = harness.session.state.get();
    await harness.session.state.set({ count: 1 });

    This does not affect the tool-facing harness context, which continues to expose state / getState / setState / updateState alongside session.state.

    mastracode is updated to set browser settings via session.state.set().

  • Moved the observational-memory model accessors off the Harness onto session.om. Reading and switching the observer/reflector models and reading observation/reflection thresholds now live on the session, next to the state they read and write. (#18200)

    Before

    const observer = harness.getObserverModelId();
    await harness.switchObserverModel({ modelId: 'openai/gpt-4o' });

    After

    const observer = harness.session.om.observer.modelId();
    await harness.session.om.observer.switchModel({ modelId: 'openai/gpt-4o' });

    The accessors are grouped by role under session.om.observer and session.om.reflector, each exposing modelId(), threshold(), resolvedModel(), and switchModel({ modelId }).

    Removed Harness.getObserverModelId, getReflectorModelId, getObservationThreshold, getReflectionThreshold, getResolvedObserverModel, getResolvedReflectorModel, switchObserverModel, and switchReflectorModel.

    mastracode is updated to consume the new API: the /om command and status line now read and switch observer/reflector models via session.om.

  • Moved the run-control surface off the Harness onto the Session. Sending messages and signals, steering, following up, aborting, responding to tool suspensions, and saving system reminders now live on the session that owns the run state they drive, instead of being delegated through the Harness. This is the final step of the single-session extraction series and a prerequisite for the upcoming multi-session (createSession) work: every per-session operation now lives on Session, while the Harness retains only genuinely shared machinery (agent, config builders, storage/lock gateway), which it injects into each session via the SessionMachinery provider. (#18213)

    Before

    await harness.sendMessage({ content: 'hello' });
    harness.sendSignal({ content: 'steer the run' });
    harness.abort();
    await harness.respondToToolSuspension({ toolCallId, approved: true });

    After

    const session = await harness.createSession();
    await session.sendMessage({ content: 'hello' });
    session.sendSignal({ content: 'steer the run' });
    session.abort();
    await session.respondToToolSuspension({ toolCallId, approved: true });

    Removed Harness.sendMessage, Harness.sendSignal, Harness.sendNotificationSignal, Harness.steer, Harness.followUp, Harness.abort, Harness.respondToToolSuspension, Harness.saveSystemReminderMessage, and Harness.waitForCurrentThreadStreamIdle. The Session reaches Harness-owned machinery through the injected SessionMachinery provider, so the heavy run loop is still constructed and owned by the Harness while being parameterized by the session it runs on.

    mastracode is updated to consume the new API: the TUI run loop, slash-command dispatch, goal lifecycle, prompt handlers, and headless entry points all drive run-control through the session returned by harness.createSession().

  • The Harness event bus now lives on the Session. Each Session owns its own listeners and emit pipeline (session.subscribe() / internal session.emit()), so events emitted on one session are delivered only to that session's subscribers — never to another session's. This is the isolation foundation for serving a single Harness to multiple concurrent sessions (e.g. one Harness backing many channel threads). (#18213)

    Breaking (Harness is under active development): Harness.subscribe() is removed. Subscribe on the session instead:

    - harness.subscribe(listener)
    + harness.session.subscribe(listener)

    Session subsystems (mode/model/om/permissions/subagents/state) no longer receive an injected emit callback — they emit directly to their session's bus. mastracode is updated to subscribe via harness.session.subscribe().

  • Add multi-tenant filtering and candidate identity to the datasets domain. (#18314)

    DatasetRecord, DatasetItem, DatasetItemRow, CreateDatasetInput, and the filters on ListDatasetsInput / ListDatasetItemsInput now expose optional organizationId and projectId, matching the per-row tenancy contract already used by the observability domain. Dataset items inherit tenancy from their parent dataset automatically — they cannot be set per-call.

    DatasetRecord and CreateDatasetInput also gain two new optional identity fields, candidateKey and candidateId, for use cases that need a stable per-incident identity at the dataset level (such as auto-materialized candidate datasets).

    The DatasetItemSource['type'] union now includes 'candidate-screener' so externally-materialized items can be distinguished from user-uploaded ones.

    DATASETS_SCHEMA and DATASET_ITEMS_SCHEMA gain matching nullable columns, and DatasetsInMemory persists and filters on them.

    DatasetsManager.create() accepts the new optional fields, and DatasetsManager.list() accepts an optional filters arg that forwards to the storage layer.

    Before

    const dataset = await storage.createDataset({ name: 'goldens/checkout' });
    const items = await storage.listDatasets({ pagination: { page: 0, perPage: 20 } });

    After

    const dataset = await storage.createDataset({
      name: 'candidates/missing-tool-call/incident-123',
      organizationId: 'org_abc',
      projectId: 'project_xyz',
      candidateKey: 'missing-tool-call',

...

Read more

June 19, 2026

Choose a tag to compare

@PaulieScanlon PaulieScanlon released this 19 Jun 15:44

Highlights

Faster long-thread resume via improved state signal restoration

@mastra/core now restores state signals without scanning every message, making it significantly cheaper and faster to resume very long threads.

More reliable signal turn handling in agents

Fixes to agent signal drains ensure pending signals are recorded through the canonical signal transcript path and response message IDs rotate consistently—preventing follow-up signal turns from attaching to the wrong assistant response.

Clearer system guidance around automatic state signals

New system guidance clarifies that browser and task-list state signals are automatic context updates rather than user instructions, reducing prompt confusion and accidental instruction-following.

Redesigned Studio panel & code-surface UI primitives in @mastra/playground-ui

Adds reusable UI support for revamped Studio panels, including true resizable panels with smooth open/close, mobile drawer behavior via PanelDrawer, a new useIsMobile hook, and CodeBlock.actions for header controls; plus new popover collision-avoidance alignment controls.

WorkOS OAuth login fix (PKCE verifier cookie missing)

@mastra/auth-workos fixes an OAuth/SSO login failure affecting both single-auth and dual-auth setups, restoring reliable WorkOS sign-in flows.

Breaking Changes

  • None called out in this changelog.

Changelog

@mastra/core@1.45.0

Minor Changes

Patch Changes

  • Improved state signal restoration so long threads can resume without scanning every message. (#18182)

  • Fix agent signal drains so pending signals are recorded through the canonical signal transcript path and consistently rotate the response message id. This prevents follow-up signal turns from being attached to the previous assistant response and helps the agent see the latest completed step before continuing. (#18105)

  • Add system guidance explaining that browser and task-list state signals are automatic context updates, not user instructions. (#18163)

@mastra/acp@0.3.0

Minor Changes

Patch Changes

@mastra/agent-browser@0.4.0

Minor Changes

Patch Changes

@mastra/agent-builder@1.1.0

Minor Changes

Patch Changes

@mastra/agentcore@0.3.0

Minor Changes

Patch Changes

@mastra/agentfs@0.2.0

Minor Changes

Patch Changes

@mastra/ai-sdk@1.5.0

Minor Changes

Patch Changes

@mastra/arize@1.3.0

Minor Changes

Patch Changes

@mastra/arthur@0.4.0

Minor Changes

Patch Changes

@mastra/astra@1.1.0

Minor Changes

Patch Changes

@mastra/auth@1.1.0

Minor Changes

Patch Changes

@mastra/auth-auth0@1.2.0

Minor Changes

Patch Changes

@mastra/auth-better-auth@1.1.0

Minor Changes

Patch Changes

@mastra/auth-clerk@1.2.0

Minor Changes

Patch Changes

@mastra/auth-cloud@1.2.0

Minor Changes

Patch Changes

@mastra/auth-firebase@1.1.0

Minor Changes

Patch Changes

@mastra/auth-neon@0.3.0

Minor Changes

Patch Changes

@mastra/auth-okta@0.1.0

Minor Changes

Patch Changes

@mastra/auth-studio@1.3.0

Minor Changes

Patch Changes

@mastra/auth-supabase@1.1.0

Minor Changes

Patch Changes

@mastra/auth-workos@1.6.0

Minor Changes

Patch Changes

  • Fix WorkOS OAuth login failing with "PKCE verifier cookie missing" error. SSO login now works correctly for both single auth and dual auth configurations. (#18035)

@mastra/azure@0.3.0

Minor Changes

Patch Changes

@mastra/blaxel@0.5.0

Minor Changes

Patch Changes

@mastra/braintrust@1.2.0

Minor Changes

Patch Changes

@mastra/brightdata@0.3.0

Minor Changes

Patch Changes

@mastra/browser-firecrawl@0.2.0

Minor Changes

Patch Changes

@mastra/browser-viewer@0.2.0

Minor Changes

Patch Changes

@mastra/chroma@1.1.0

Minor Changes

Patch Changes

@mastra/claude@0.3.0

Minor Changes

Patch Changes

@mastra/clickhouse@1.11.0

Minor Changes

Patch Changes

@mastra/client-js@1.26.0

Minor Changes

Read more

June 12, 2026

Choose a tag to compare

@PaulieScanlon PaulieScanlon released this 12 Jun 14:48

Highlights

Trusted “system actor” execution for workflows, tools, memory, and agents

You can now run server-side/background work (cron jobs, schedulers, queues) as a trusted actor across workflow.execute(), tool.execute(), agent.generate()/stream(), and memory thread FGA checks—preserving tenant-scoped authorization while avoiding JWT/human membership requirements (requires organizationId in request context).

SignalProvider framework + declarative agent signal wiring (webhooks/polling/subscriptions)

A new SignalProvider abstraction enables building notification signal providers with built-in subscription tracking, polling lifecycle management, and webhook support; includes WebhookSignalProvider and also powers higher-level bundled providers like TaskSignalProvider.

Agent-agnostic interactive tools via native tool suspension (ask_user, submit_plan) + new Harness suspension API

ask_user and submit_plan now suspend using the generic tool-suspension primitive, so they work on any agent (not just Harness). Harness now surfaces tool_suspended events and resumes via respondToToolSuspension, with support for multiple concurrent suspensions.

ThreadState storage domain unlocks durable, agent-agnostic task tools

Task tools (task_write/update/complete/check) are now agent-agnostic by storing task state in a new per-thread threadState storage domain (keyed by (threadId, type)), projected to the state-signal lane via TaskStateProcessor for delta-first updates and better prompt-cache behavior; LibSQL adds ThreadStateLibSQL for durability across restarts.

New infrastructure/storage options: Postgres vNext observability + ClickHouse replication + durable harness sessions

Postgres gains PostgresStoreVNext with a dedicated, partitioned, insert-only observability domain (separate observability connection) for scalable spans/logs/metrics OLAP; ClickHouse adds opt-in replicated tables for multi-replica clusters; Harness v1 sessions become durable across backends (including new LibSQL support).

New packages: OpenAI Agents SDK + Railway Sandboxes + VoyageAI embeddings/reranking

Added @mastra/openai to run OpenAI Agents SDK agents through Mastra’s generate()/stream() with tracing/usage continuity; added @mastra/railway as a WorkspaceSandbox provider for Railway Sandboxes; added @mastra/voyageai for VoyageAI embeddings (incl. multimodal) and rerankers.

Developer experience: batching PubSub delivery + tool hooks + cheaper tool discovery

PubSub now supports per-subscriber batched/coalesced delivery (SubscribeOptions.batch) and flush(); agents/workspaces gain beforeToolCall/afterToolCall hooks; ToolSearchProcessor adds autoLoad (skip load round-trip) and restart-safe storage: 'context' mode.

Breaking Changes

  • Removed harness.respondToQuestion(...) → use harness.respondToToolSuspension(...).
  • Removed ask_question event → listen for tool_suspended and read event.suspendPayload.
  • Removed Harness question-related APIs/types (registerQuestion, pendingQuestion, HarnessQuestion* types); pendingSuspension becomes pendingSuspensions: Map<toolCallId, ...>.
  • Removed harness.respondToPlanApproval(...) and plan_approval_required/plan_approved events → resume submit_plan via respondToToolSuspension({ toolCallId, resumeData: { action, feedback } }).

Changelog

@mastra/core@1.42.0

Minor Changes

  • You can now execute workflows, tools, and memory thread checks as a trusted actor, such as a background job or scheduled task. Pass an actor object to identify the system process making the call while keeping fine-grained authorization checks tenant-scoped. (#17484)

    const actor = { actorKind: 'system', sourceWorkflow: 'nightly-sync' } as const;
    
    await workflow.execute({ ...executeOptions, actor });
    await tool.execute(input, { ...toolOptions, actor });
    await MastraMemory.checkThreadFGA({ ...threadFGAOptions, actor });
  • Added an actor signal to core FGA checks for trusted server-side membership bypasses. (#17483)

    const actor = { actorKind: 'system', sourceWorkflow: 'nightly-workflow' } as const;
    await checkFGA({ ...fgaOptions, requestContext, actor });
    await requireFGA({ ...fgaOptions, requestContext, actor });
  • Enabled persistent storage for durable harness sessions across storage backends. Harness v1 can now resolve its session store from a configured storage adapter. (#17712)

  • Added the actor option to agent generate() and stream() invocations so trusted background work can run without a JWT or human membership. (#17487)

    const requestContext = new RequestContext();
    requestContext.set('organizationId', 'org_123');
    
    await agent.generate('Process daily report', {
      requestContext,
      actor: { actorKind: 'system', sourceWorkflow: 'daily-report-cron' },
    });

    Mastra denies trusted actor FGA checks when the request context does not include an organizationId.

  • Added SignalProvider abstraction for building notification signal providers. Enables declarative signal wiring in Agent config with built-in subscription tracking, polling lifecycle, and webhook support. Includes WebhookSignalProvider as a proof-of-concept. (#17577)

    Writing a signal provider:

    import { SignalProvider } from '@mastra/core/signals';
    import type { SignalSubscription } from '@mastra/core/signals';
    
    class SlackSignalProvider extends SignalProvider<'slack-signals'> {
      readonly id = 'slack-signals' as const;
      readonly pollInterval = 30_000; // poll every 30s
    
      async poll(subscriptions: SignalSubscription[]) {
        for (const sub of subscriptions) {
          const messages = await fetchNewMessages(sub.externalResourceId);
          if (messages.length > 0) {
            await this.notify(
              { source: 'slack', kind: 'new-message', summary: `${messages.length} new messages` },
              { threadId: sub.threadId, resourceId: sub.resourceId },
            );
          }
        }
      }
    }

    Declarative wiring:

    import { Agent } from '@mastra/core/agent';
    
    const agent = new Agent({
      signals: [new SlackSignalProvider()],
      // ... other config
    });
  • Added optional getUsers(userIds) batch lookup method to IUserProvider. Auth providers can implement it to resolve multiple users in a single call; providers that don't implement it continue to work via per-id getUser fallback. (#17205)

    // optional batch lookup, returns results positionally aligned to userIds
    const users = await provider.getUsers?.(['u_1', 'u_2', 'u_3']);
  • Added MCP server Fine-Grained Authorization mapping overrides for tool authorization. (#17529)

    Use the new fga option on MCPServer to customize the resource and permission mappings used for tools/list and tools/call checks without changing the Mastra instance-level tool mapping used by internal agent and workflow tool execution.

    const server = new MCPServer({
      name: 'My Server',
      version: '1.0.0',
      tools: { getData },
      fga: {
        resourceMapping: {
          tool: {
            fgaResourceType: 'user',
            deriveId: ({ user }) => user.id,
          },
        },
        permissionMapping: {
          'tools:execute': 'read',
        },
      },
    });
  • Made the ask_user built-in tool agent-agnostic and removed the Harness question channel in favor of native tool suspension. (#17806)

    ask_user now pauses through the same tool-suspension primitive used by every other interactive tool, so it works on any agent — not just inside a Harness. The Harness no longer has a separate question channel; instead it surfaces these pauses through the generic tool_suspended event and resumes them with respondToToolSuspension.

    Breaking changes

    • Removed harness.respondToQuestion(...). Use harness.respondToToolSuspension(...) instead.
    • Removed the ask_question event. Listen for tool_suspended and read the question from event.suspendPayload.
    • Removed registerQuestion from HarnessRequestContext, the HarnessDisplayState.pendingQuestion field, and the HarnessQuestionAnswer, HarnessQuestionOption, and HarnessQuestionSelectionMode types.
    • HarnessDisplayState.pendingSuspension (a single object or null) is now HarnessDisplayState.pendingSuspensions, a Map keyed by toolCallId. This lets the display state hold several parked prompts at once, so resuming one parallel ask_user no longer hides the others. Read a specific prompt with displayState.pendingSuspensions.get(toolCallId).

    respondToToolSuspension accepts an optional toolCallId so concurrently suspended tools (for example, parallel ask_user calls) can each be answered independently.

    harness.abort() now clears any pending tool suspensions, so a run parked in a suspend() (e.g. an unanswered ask_user) can be aborted instead of staying parked forever. A new harness.hasPendingSuspensions() method reports whether the harness is awaiting a resume — useful because a suspended run nulls its internal abort controller, so isRunning() returns false while the run is still pending.

    Before

    harness.subscribe(event => {
      if (event.type === 'ask_question') {
        harness.re...
Read more

June 4, 2026

Choose a tag to compare

@PaulieScanlon PaulieScanlon released this 05 Jun 09:10

Highlights

Per-request Workspace sandboxes (multi-tenant + isolation)

@mastra/core Workspace sandbox can now be a resolver function, enabling per-request sandbox routing for multi-tenant deployments with isolated working directories/permissions; includes prompt-safe “stable placeholder” instructions by default with an opt-in instructions.dynamicSandbox mode.

Sandbox continuity for background processes via sandboxCacheKey

When using dynamic sandboxes, sandboxCacheKey lets background process tools (execute_command with background: true, get_process_output, kill_process) reliably attach to the same sandbox across follow-up requests, with cache management via workspace.clearSandboxCache().

Unified “stream until idle” behavior via untilIdle option (core + server + client SDKs)

stream()/resumeStream() now support untilIdle (boolean or { maxIdleMs }) to keep streams open across background-task continuations; the same untilIdle request field is supported on server endpoints and client SDKs.

Deprecation: dedicated *UntilIdle methods/endpoints

streamUntilIdle() and resumeStreamUntilIdle() (and the dedicated /stream-until-idle and /resume-stream-until-idle endpoints) are deprecated in favor of stream(..., { untilIdle: true }) / resumeStream(..., { untilIdle: true }).

Breaking Changes

  • None noted in this changelog.

Changelog

@mastra/core@1.41.0

Minor Changes

  • Workspace sandbox now accepts a resolver function for per-request sandboxes. (#16048)

    Before: sandbox: WorkspaceSandbox (static, same sandbox for every request)
    After: sandbox: WorkspaceSandbox | (({ requestContext }) => WorkspaceSandbox) (static or per-request)

    This enables per-request sandbox routing from a single Workspace — useful for multi-tenant deployments where each user/role needs an isolated working directory or different execution permissions.

    const workspace = new Workspace({
      sandbox: ({ requestContext }) => {
        const userId = requestContext.get('user-id') as string;
        return new LocalSandbox({ workingDirectory: `/workspaces/${userId}` });
      },
    });

    When using a resolver, the caller owns the returned sandbox's lifecycle — the Workspace will not call start() or destroy() on it. mounts throws an INVALID_CONFIG error with a resolver, and lsp: true is disabled with a warning because both require a concrete sandbox instance up front.

    Stable prompts by default

    Building workspace instructions no longer calls a sandbox resolver. Resolver-backed sandboxes contribute stable placeholder text to the agent's system message, so constructing the prompt never provisions a caller-owned sandbox and the prompt stays cache-friendly. Opt into concrete per-request instructions with instructions.dynamicSandbox:

    const workspace = new Workspace({
      sandbox: ({ requestContext }) => resolveSandbox(requestContext),
      instructions: { dynamicSandbox: 'resolve' }, // or a ({ requestContext }) => string function
    });

    Background process continuity

    Set sandboxCacheKey to keep execute_command({ background: true }), get_process_output, and kill_process on the same sandbox across follow-up requests — continuity is keyed by a stable id rather than the RequestContext instance:

    const workspace = new Workspace({
      sandbox: ({ requestContext }) => resolveSandbox(requestContext),
      sandboxCacheKey: ({ requestContext }) => requestContext.get('thread-id') as string,
    });

    Failed sandbox resolver calls are removed from the cache so later calls can retry. Use workspace.clearSandboxCache(cacheKey) to drop a keyed sandbox reference when your own lifecycle code has destroyed or replaced that sandbox.

    When background process tools cannot find a PID on a dynamic sandbox without sandboxCacheKey, the tool output now points to sandboxCacheKey so callers can fix continuity across follow-up requests.

Patch Changes

  • Added untilIdle option to stream() and resumeStream() methods. Pass untilIdle: true (or untilIdle: { maxIdleMs: 60_000 }) to keep the stream open across background-task continuations — same behavior as the now-deprecated streamUntilIdle() method. (#17536)

    Example:

    const result = await agent.stream('Research solana for me', {
      untilIdle: true,
      memory: { thread: 't1', resource: 'u1' },
    });

    Deprecated streamUntilIdle() and resumeStreamUntilIdle() — they still work but now delegate internally to stream({ untilIdle: true }).

@mastra/acp@0.2.1

Patch Changes

  • Fixed ACP tools to keep their default session alive across executions. (#17516)

@mastra/client-js@1.23.2

Patch Changes

  • The /agents/:agentId/stream and /agents/:agentId/resume-stream endpoints now accept an untilIdle field in the request body. When set, the stream stays open across background-task continuations (same behavior as the /stream-until-idle endpoint). The dedicated /stream-until-idle and /resume-stream-until-idle endpoints remain available but are deprecated. (#17536)

    Server example:

    // POST /api/agents/:agentId/stream
    fetch(`/api/agents/${agentId}/stream`, {
      method: 'POST',
      body: JSON.stringify({
        messages: [{ role: 'user', content: 'Research solana for me' }],
        untilIdle: true, // or { maxIdleMs: 60000 }
      }),
    });

    Client SDK: streamUntilIdle() and resumeStreamUntilIdle() are deprecated — use stream(messages, { untilIdle: true }) instead.

@mastra/playground-ui@32.0.2

Patch Changes

  • Fixed dropdown, combobox, select, and popover popups silently failing to render when opened outside a side panel (most visibly the model and provider pickers in the chat composer). The shared portal-container resolver now always falls back to the document body instead of leaking an unrenderable value. (#17560)

  • Fixed combobox popups so model picker dropdowns open correctly outside side dialogs. (#17556)

@mastra/react@0.5.2

Patch Changes

  • The /agents/:agentId/stream and /agents/:agentId/resume-stream endpoints now accept an untilIdle field in the request body. When set, the stream stays open across background-task continuations (same behavior as the /stream-until-idle endpoint). The dedicated /stream-until-idle and /resume-stream-until-idle endpoints remain available but are deprecated. (#17536)

    Server example:

    // POST /api/agents/:agentId/stream
    fetch(`/api/agents/${agentId}/stream`, {
      method: 'POST',
      body: JSON.stringify({
        messages: [{ role: 'user', content: 'Research solana for me' }],
        untilIdle: true, // or { maxIdleMs: 60000 }
      }),
    });

    Client SDK: streamUntilIdle() and resumeStreamUntilIdle() are deprecated — use stream(messages, { untilIdle: true }) instead.

@mastra/server@1.41.0

Minor Changes

  • The /agents/:agentId/stream and /agents/:agentId/resume-stream endpoints now accept an untilIdle field in the request body. When set, the stream stays open across background-task continuations (same behavior as the /stream-until-idle endpoint). The dedicated /stream-until-idle and /resume-stream-until-idle endpoints remain available but are deprecated. (#17536)

    Server example:

    // POST /api/agents/:agentId/stream
    fetch(`/api/agents/${agentId}/stream`, {
      method: 'POST',
      body: JSON.stringify({
        messages: [{ role: 'user', content: 'Research solana for me' }],
        untilIdle: true, // or { maxIdleMs: 60000 }
      }),
    });

    Client SDK: streamUntilIdle() and resumeStreamUntilIdle() are deprecated — use stream(messages, { untilIdle: true }) instead.

Patch Changes

Other updated packages

The following packages were updated with dependency changes only:

Read more

June 3, 2026

Choose a tag to compare

@PaulieScanlon PaulieScanlon released this 05 Jun 09:09

Highlights

Gateway Embedding Routing via mastra/provider/model IDs

@mastra/core adds support for routing embeddings through the Gateway using mastra/provider/model identifiers (e.g., new ModelRouterEmbeddingModel('mastra/__GATEWAY_OPENAI_EMBEDDING_MODEL__')).

Breaking Changes

  • None noted in this changelog (other packages were dependency-only updates).

Changelog

@mastra/core@1.40.0

Minor Changes

  • Added support for Gateway embedding routing with mastra/provider/model IDs: (#17469)

    const embedder = new ModelRouterEmbeddingModel('mastra/__GATEWAY_OPENAI_EMBEDDING_MODEL__');

Other updated packages

The following packages were updated with dependency changes only: