Skip to content

Update dependency @mastra/core to v1 - #12

Open
renovate[bot] wants to merge 1 commit into
mainfrom
renovate/mastra-core-1.x
Open

renovate[bot] wants to merge 1 commit into
mainfrom
renovate/mastra-core-1.x

Conversation

@renovate

@renovate renovate Bot commented Apr 15, 2026 •

Copy link
Copy Markdown
Contributor

Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Change Age Confidence
@mastra/core (source) ^0.8.0 → ^0.8.0 || ^1.0.0 age confidence
@mastra/core (source) 0.8.3 → 1.82.0 age confidence

Release Notes

mastra-ai/mastra (@​mastra/core)

v1.82.0

Compare Source

Minor Changes
  • An agent's scopes function can now narrow or replace a run's scopes. It receives the run's incoming scopes (from the call, defaultOptions and request context) and returns the run's final set. A static scopes list is still added to the incoming scopes. The function must keep any resource: or thread: scope it was given; dropping one throws. (#​28353)

    const agent = new Agent({
      id: 'support',
      name: 'Support',
      instructions: 'Answer support questions.',
      model,
      // Keep identity scopes, drop anything else the caller sent, and add the tenant's org.
      scopes: ({ scopes, requestContext }) => [
        ...scopes.filter(scope => typeof scope === 'string' && /^(resource|thread):/.test(scope)),
        `org:${requestContext.get('orgId')}`,
      ],
    });
  • Added requestId, serviceName, and experimentId filters to advanced trace queries. Use them at trace scope or inside spans.some / spans.none with eq, ne, in, notIn, exists, and notExists. (#​29194)

    const result = await mastraClient.queryTraces({
      timeRange: { from: '2026-11-01T00:00:00.000Z', to: '2026-11-02T00:00:00.000Z' },
      where: {
        op: 'and',
        args: [
          { op: 'eq', left: { path: 'requestId' }, right: { literal: 'req-42' } },
          { spans: { some: { op: 'eq', left: { path: 'serviceName' }, right: { literal: 'payments' } } } },
        ],
      },
    });
  • Added onDecision to ClassifierProcessor for writing guardrail decisions to your own audit log (#​28985). It runs after each classification, including failed classifier calls, and receives the outcome (allowed, filtered or aborted), the answers or the error, the call duration, and the ids of the message, thread, resource or run. (#​29070)

    new ClassifierProcessor({
      classifier: safety,
      onResult,
      onDecision: async decision => {
        await auditLog.insert({
          threadId: decision.ids.threadId,
          outcome: decision.outcome,
          answers: decision.result ? decision.result.answers : null,
          error: decision.result ? null : String(decision.error),
        });
      },
    });

    abort in onResult now accepts tripwire metadata, so the caller can tell which policy blocked the request:

    onResult: (answers, { abort }) => {
      if ((answers.unsafe.probability ?? 1) > 0.8) {
        abort('Blocked', { metadata: { policy: 'unsafe' } });
      }
    };

    A failed classifier call under errorStrategy: 'strict' now sets tripwire metadata { processorId, classifierId, code: 'CLASSIFIER_FAILED' }. On agents that aren't durable, stopping a streaming reply now cancels a classifier call that is still running.

  • Fixed evented durable agents stopping when the API process shuts down. Workers now keep running the agent, finish cleanup, and free the thread, so the next message starts right away. (#​28951)

    Each run keeps its own ownership. Nested durable agents and runs that share a RequestContext no longer pick up each other's ownership. Canceling a suspended run still notifies listeners, and a late cancel can no longer stop a newer run of the same agent.

    This needs storage that supports run fencing. Other shared setups keep their current behavior and log a warning once per agent: API shutdown can still stop remote workers. Single-process setups are unchanged.

    To enable worker continuity across API loss, use a shared fencing-capable adapter, such as PostgresStore from @mastra/pg, and a shared pubsub transport:

    new Mastra({
      storage: new PostgresStore({ id: 'shared-storage', connectionString }),
      pubsub,
      agents: { agent: createEventedAgent({ agent, pubsub }) },
    });
  • Added getStepResultAsync to workflow step, condition, sleep, and .map() contexts, and deprecated getStepResult. (#​26999)

    getStepResultAsync returns the same value as getStepResult, wrapped in a promise. Switching now prepares workflows for upcoming storage changes that load step results on demand instead of keeping every result in memory. getStepResult keeps working and will be removed in a future major release.

    // Before
    const step2 = createStep({
      execute: async ({ getStepResult }) => {
        const step1Result = getStepResult(step1);
      },
    });
    
    // After
    const step2 = createStep({
      execute: async ({ getStepResultAsync }) => {
        const step1Result = await getStepResultAsync(step1);
      },
    });

    If your tests build step execute params by hand, add a getStepResultAsync function to them.

  • Changes to an experimental, upcoming memory feature. (#​28353)

    experimental_subconscious and the Memory knowledge option are removed: setting either now throws, saying to attach Knowledge to the agent instead (knowledge: false is still accepted). Subconscious and KnowledgeSemanticIndexCoordinator stay exported from @mastra/memory so older code still loads, and throw when constructed. @mastra/core/knowledge exports Knowledge with its option and result types and the Knowledge storage domain; the tool, pin and semantic-index helpers are internal.

    Agent scopes are plain "<type>:<value>" strings that say who the run is; what the run may read or write comes from whoever consumes them, such as Knowledge's scope declarations.

    A message can carry its own scopes: session.sendMessage({ content, scopes }). A run's scopes are fixed when it starts, so a message whose scopes differ from the running run's waits for it to end, then starts its own run in the same conversation. sendSignal reports this as { action: 'blocked', reason: 'scope-mismatch', runId }. queueMessage, queueSignal, sendNotificationSignal and steer take scopes the same way: a queued or steering message, or a notification, whose scopes differ from the running run's starts its own run with its scopes after that run instead of joining it. A deferred notification keeps the scopes it was sent with. Scopes are set in process only: remote callers (the agent controller HTTP routes and @mastra/client-js) can't send them. To derive scopes from a request, give the agent a scopes function that reads trusted request context. When the thread's claimed owner runs in another process, a message with scopes isn't queued: sendMessage rejects with an error saying scopes don't cross processes. Messages without scopes still join that owner's run.

    Attach Knowledge with new Agent({ knowledge, scopes }). Knowledge needs scopes, the scope types its runs may hold and who gets which role on them: new Knowledge({ storage, scopes: { ...knowledgeMemoryScopes() } }) declares Memory's thread:/resource: scopes and an org: scope; {} declares none. A run holding a scope that isn't declared fails. Tools are off by default: tools: true gives the read and write tools, tools: { read: true } the read tools only, and pinnedContext: true (or { maxPins, maxTokens }) delivers pinned pages every turn and adds knowledge_pin. knowledge.readScopeFor(principals) returns what holders of those scopes can read, for host code reading the store directly. knowledge.explainAccess(runScopes, { scope } | { page }) says why a run can or can't reach a scope or page; it's host-only, so never expose it through an agent tool or an unauthenticated route. knowledge.onChange(listener) reports each committed write made through knowledge.getStorage() (tools included) by id, for live UIs; re-read each page under the viewer's read scope before showing it. A scope declaration can carry a displayLabel for UIs, and knowledge.scopeDeclaration(key) returns a declaration's display fields.

    Knowledge is a tree of pages. The tools are five: knowledge_search, knowledge_read (a page with its notes, or a scope's pages), knowledge_page (op: 'write' | 'move' | 'merge'), knowledge_note (op: 'add' | 'restamp' | 'remove') and knowledge_pin (op: 'pin' | 'unpin'). Notes are at most about 250 tokens and page descriptions about 100; long text goes in a page body.

    Search is keyword search by default. Give Knowledge a vector store and an embedder and knowledge_search blends keyword and semantic results; pages and notes written through the tools are embedded as they're written. Every semantic hit is re-checked against what the run can read before it's ranked, so hidden content never fills result slots. Content written straight to the store isn't embedded.

  • Added knowledge_list, a Knowledge read tool that lists the page tree under a starting point, so an agent can see how a scope or section is organized before searching. Each entry has the ref knowledge_read accepts, its name and child count; pass descriptions: true to add page descriptions. It goes up to 3 levels deep, takes a limit and a cursor for wide levels, and says when deeper levels were cut off. A result also stops at the same token cap as a knowledge_read body; more says how many entries were left out and gives the cursor that continues exactly where it stopped, at any depth. Scopes the run can't read are left out. tools: { read: true } includes it. (#​29207)

    // Agent tool call
    knowledge_list({ from: 'docs:acme#guides', depth: 2 });
    // → { from: { ref: 'docs:acme#guides', name: 'Guides' },
    //     entries: [{ ref: 'docs:acme#guides/observability', name: 'Observability', depth: 1, children: 2 }, …] }

    Knowledge storage adapters gained countChildPages({ parentIds, read }), which counts the readable child pages under each parent in one query.

  • Agents can pin a listing of the Knowledge page tree with knowledge_pin. Pass kind: 'list' and an optional depth (1 to 3, default 1) to pin the pages under a page instead of the page itself: (#​29263)

    { "op": "pin", "kind": "list", "depth": 2, "page": "docs:acme#guides", "audience": ["org:acme"] }

    A page can hold a page pin and a listing pin at once; each is pinned and unpinned on its own, and pinning a listing again sets its new depth. Stored listings follow the same rules as config listings: only pages the run can read, one pin slot, shared pinnedContext.maxTokens. A stored listing that repeats a config listing is shown once.

    Knowledge storage adapters store a pin kind (page or list) and a list pin's depth. Adapters report this with the supportsPinKinds capability. With a storage adapter from before this release, page pins keep working and pinning a listing is refused before anything is stored, with a message to upgrade the adapter.

  • Pin a listing of the Knowledge page tree into an agent's context. A scope's pinnedContext now also takes { list, depth? } entries, so every run holding the scope sees the pages under a scope or page (ref, name and child count each) without calling knowledge_list: (#​29263)

    scopes: {
      'org:$orgId': { pinnedContext: [{ list: 'docs:$orgId#guides', depth: 1 }] },
    }

    A listing shows only pages the run can read, updates when a listed page is added, renamed or removed, takes one pin slot and shares pinnedContext.maxTokens with pinned pages. A listing cut short ends with a knowledge_list pointer to the rest.

  • knowledge_read reads long pages by line. A body that fits in one read (1,000 tokens) comes back whole as body. A longer one comes back as numbered lines (12│text) with the range they cover and the page's lineCount, ending at the last whole line that fits, and more says where to read on with from: { line }. A line too long for one read, such as a wide table row, is cut by column (from: { line, col }). An agent can also ask for the body's outline (headings with their line numbers and sizes), read one section, or match one or more terms like grep: only the lines that contain them, with context lines before and after (default 2). (#​29207)

    knowledge_read({ page: 'docs:acme#models/anthropic', outline: true });
    knowledge_read({ page: 'docs:acme#models/anthropic', section: 'contextManagement' });
    knowledge_read({ page: 'docs:acme#models/anthropic', match: ['excludeTools', 'cacheControl'], context: 3 });
    knowledge_read({ page: 'docs:acme#models/anthropic', from: { line: 120 } });

    Every Knowledge read result now lists anything it cut short in more, with how to get the rest: the body, notes, child pages (continue with knowledge_list), notes that mention the page (new mentionedInCursor), and search results at their limit.

    A whole-page read keeps a page's notes, and the notes on other pages that mention it, only while each list stays under 1,000 tokens. A longer list is left out and counted in more; read it on its own with notes: true or mentionedIn: true. A read with cursor or mentionedInCursor now returns only those notes, without repeating the body.

    Every part of a body a read returns comes with its range. knowledge_page takes a range with expectedVersion and replaces only those lines with body; an empty body removes them. A range may be the one a read returned or a smaller one inside it, but not wider than one read from its first line shows, so text the writer never saw can't be overwritten. A body write without a range is refused when the stored body is longer than one read returns, and so is a body that still carries the read's line numbers.

    const { page } = await knowledge_read({ page: 'docs:acme#models/anthropic', section: 'Setup' });
    knowledge_page({ op: 'write', page: page.ref, body: editedSetup, range: page.range, expectedVersion: page.version });

    The per-result cap is configurable: tools: { read: { maxTokens: 3000 }, write: true } sets how many estimated tokens one knowledge_read body window or match result, and one knowledge_list result, returns (default 1,000, at least 100). The write guard for line ranges follows the same cap.

    const knowledge = new Knowledge({ scopes, tools: { read: { maxTokens: 3000 }, write: true } });
  • Added vector and hybrid workspace search for vector stores that generate embeddings themselves, so no embedder is needed. (#​27022)

    With MongoDB Automated Embedding, MongoDB embeds each file when it is indexed and the search string when you query:

    const workspace = new Workspace({
      filesystem: new LocalFilesystem({ basePath: './workspace' }),
      vectorStore: new MongoDBVector({ id: 'search', uri, dbName, autoEmbed: { model: 'voyage-4' } }),
    });
    
    await workspace.rebuildSearchIndex(['docs']);
    await workspace.search('how do I reset a password?');

    The index is named <workspace id>_search_selfembed unless you set searchIndexName. Workspaces that pass an embedder keep working unchanged.

  • Added opt-in Editor ownership and versioned persistence for static model fallback lists. (#​27287)

    new Agent({
      id: 'support-agent',
      name: 'Support agent',
      instructions: 'Help users resolve support requests.',
      model: [
        { id: 'primary', model: 'openai/gpt-4o' },
        { id: 'fallback', model: 'openai/gpt-4o-mini' },
      ],
      editor: { model: true },
    });
  • Added time to first token and time to first chunk measures to trace and span aggregates, so dashboards can chart streaming latency next to duration. (#​29055)

    The new measures are timeToFirstToken.avg, .min, .max, .p50, .p90, .p95, .p99, and .count, plus the same set for timeToFirstChunk. Traces and spans without a recorded value are skipped instead of counted as 0, and .count reports how many had one. Stores opt in through the aggregate-time-to-first-measures feature.

  • Suspended background tool calls can now be answered after a reload. (#​28998)

    Open request saved on the tool call. While a deferred background tool is suspended, the assistant message that holds the call keeps the open request in metadata.suspendedTools[toolCallId] (with suspendPayload, taskId, and suspendedAt). The entry is removed when the task resumes or is cancelled, so a chat that reloads the thread shows only requests that are still open.

    Resumed tools see what they asked for. A resumed background tool now receives the payload it suspended with in context.agent.suspendPayload, the same as a foreground tool:

    execute: async (input, context) => {
      const { resumeData, suspendPayload, suspend } = context.agent;
      if (!resumeData) return suspend?.({ question: 'Approve the refund?' });
      // suspendPayload is { question: 'Approve the refund?' }
    };

    Resume only the suspension you read. BackgroundTaskManager.resume() takes an optional suspendedAt, and refuses the resume when the task has resumed or suspended again since:

    await mastra.backgroundTaskManager.resume(taskId, { approved: true }, { suspendedAt: task.suspendedAt });

    The agent reports a deferred task's result after its stream ends. When a deferred background task finishes and no untilIdle stream is open for its thread, the agent now gets a background-task notification signal on the thread. An idle thread wakes and the agent reports the outcome; a run already active on the thread gets the signal on its next model request. Before, the result was only written to memory and waited for the next user message. This covers stream() calls without untilIdle and tasks resumed after they suspended, for example an MCP task answered from the chat. Streams using untilIdle are unchanged: they still run their own follow-up turn, and no signal is sent. Cancelling or aborting an untilIdle stream still means no reply for the tasks it was waiting for.

    Opt in to ending woken runs after their turn. A run that a signal starts on an idle thread still stays open until the background tasks it dispatched finish. Set the temporary wokenRunsEndAfterTurn option to end it after its own turn instead; each task then wakes the thread when it finishes. A signal's own ifIdle.streamOptions.untilIdle takes precedence either way.

    new Mastra({ backgroundTasks: { enabled: true, wokenRunsEndAfterTurn: true } });

    In the next major release, ending after the turn becomes the only behavior and wokenRunsEndAfterTurn is removed. Pass untilIdle: true in ifIdle.streamOptions to keep a woken run open; channels and agent controller sessions already do.

    Turn off the completion signal. Set notifyAgentOnFinish: false in the backgroundTasks config when your app sends its own completion signal, for example from onTaskComplete. With wokenRunsEndAfterTurn also on, your app must report the tasks of woken runs itself.

  • Added knowledge.upsertPage(), knowledge.removePage(), knowledge.addNote(), knowledge.updateNote() and knowledge.removeNote() so apps can sync outside content, such as docs, into Knowledge from server code. A page is identified by its scope and key path, so running a sync again updates the same pages and skips unchanged ones. Notes on synced pages are kept, pages are embedded when Knowledge has a vector store, and change listeners are told. (#​29159)

    await knowledge.upsertPage({ scope: 'docs:acme', path: 'guides/install', name: 'Install', body: markdown });
    await knowledge.removePage({ scope: 'docs:acme', path: 'guides/old' });
    
    const note = await knowledge.addNote({ scope: 'docs:acme', path: 'guides/install', text: 'Needs Node 20.' });
    await knowledge.updateNote({ id: note.id, text: 'Needs Node 22.' }); // returns a note with a new id
  • Fixed a recovered thread run losing its thread lease when the execution it replaced finished. Previously, when a run was recovered in another process, the original execution's exit released the shared lease while the recovered execution was still running, so the thread looked free and a new run could start on it. Each execution now holds the lease under its own generation, so only the live execution can renew or release it. (#​28947)

    Also fixed run IDs that contain the internal handover marker, or look like an encoded lease owner, being misread as a different run. Any caller-provided run ID now round-trips unchanged.

    Upgrading shared deployments

    Drain active thread runs and restart API and worker processes together. Older processes do not recognize the new lease owner format: they cannot see a run as live, even while it is renewing its lease. Follow-ups that an older process forwards to a run on this release are still delivered.

Patch Changes
  • dependencies updates: (#​29242)

  • Moved the agent stream layer (stream topics, event publishing, replay cache setup and abort requests) out of DurableAgent into a shared internal module. No behavior change; existing @mastra/core/agent/durable exports are unchanged. (#​28416)

  • Fixed resuming suspended agent runs across server instances when a process retains an earlier run segment. (#​29065)

  • Update provider registry and model documentation with latest models and providers (4d3ebbd)

  • Fixed thread aborts for durable agents on multi-server deployments. Aborting a thread on a server that didn't start the run now stops the run on whichever server executes it, as aborting by run id already did. A run parked on a suspension, such as a tool waiting for approval, is released too. (#​28345)

    agent.abortThreadStream({ resourceId, threadId });
    // or POST /agents/:agentId/threads/abort

    The server finds the run through the thread lease, so this needs a pubsub with a shared lease backend such as Redis or Valkey.

    Return values

    • The HTTP route returns { aborted: true } once the abort request is published, and an error if the lease backend can't be reached or doesn't answer in time.
    • The synchronous abortThreadStream still returns false for a run on another server, because it can't wait for the lookup.
    • An expectedRunId that doesn't match the running run leaves it alone and returns false.

    Queued input

    With clearPendingSignals: true, the server running the run clears its queued input before stopping it, as long as that run still holds the thread. Input queued for a run that has since taken over the thread is kept.

  • Updated chat and the Chat SDK adapters to ^4.44.0, so all Mastra channel packages resolve the same chat version. (#​29285)

  • Fixed "Tenant model credential policy could not be resolved: the request has no authenticated user" errors that recurred on every message in a session with a pending tool approval. When a new user message declines a pending approval, the decline now runs with the sender's request context instead of the reattached run's empty one. (#​29231)

  • Fixed durable agents (durable: true) ending the model_generation trace span without responseId and responseModel. Durable runs now record the provider's response id and model on the span, the same as non-durable agents, so a trace can be linked back to the provider's own response log. Fixes #​29053. (#​29180)

  • Improved validateStepRequestContext typing. It now accepts any object with id and requestContextSchema, not only a full Step. (#​29080)

  • Fixed skills not loading on Windows when the workspace basePath uses a short folder name such as C:\Users\ABCDEF~1\..., which os.tmpdir() can return. Optional skill folders that don't exist, like references, no longer fail with Permission denied. Fixes #​28957. (#​29078)

  • Fixed a signal sent to a thread while an agent.generate() turn was running starting a second run that could not see that turn. generate() now registers its run with the thread like stream() does, so sendSignal, sendMessage and notifications with the default ifActive: 'deliver' join the running turn. (#​28953)

    As with stream(), a thread-bound generate() now waits for a run already going on the same thread, so concurrent generate() calls on one thread run one after another instead of in parallel. A tool that calls generate() or stream() on its own thread waits for the run it is part of, so it deadlocks; a tool should use sendSignal to send a message to its own thread.

  • Fixed metadata filters in the in-memory store returning records whose nested metadata had different keys from the filter when one side contained an undefined value. (#​29101)

  • Improved how Knowledge reads fenced code and headings in page bodies, to match CommonMark: (#​29260)

    • Fenced code ends only on a fence of the same character, at least as long, with nothing after it. A block can show ```js ```` lines without breaking a page's sections, outline or embedding chunks. A fence indented four or more spaces is indented code and doesn't open or close a block.
    • A heading keeps a # that's part of its title: ## Use C# is the section "Use C#".
    • Reading the last part of a page whose body ends in a newline returns a range that knowledge_page accepts back for an edit.

    Embedding a long page now runs up to 4 chunk embeddings at once (one at a time when the embedding model doesn't support parallel calls), and one semantic search looks at no more than 1,000 distinct pages and notes.

  • Knowledge notes returned by knowledge_note, knowledge_read and knowledge_search now carry mentions, mapping each [[ref]] in the note to the page id it resolves to, so a chat can link them. Refs are resolved the way the note's write resolved them; a name that no longer resolves is left out. (#​28942)

    { "id": "n1", "text": "Escalate per [[Escalation Process]].", "mentions": { "Escalation Process": "page-esc" } }
  • knowledge_list now asks the store for only the child scopes the agent can read, and counts them in one call per level, instead of reading every child scope and dropping the unreadable ones. Listing under a scope with many private child scopes no longer loads all of them. (#​29260)

  • Knowledge pages too long for the embedder are now searchable by meaning. A long page is embedded as overlapping chunks of about 2,000 tokens, cut between paragraphs and before headings, and semantic search still returns the page once, ranked by its best chunk. Before, the embedder refused these pages and they could only be found by keyword. Rewriting a page replaces its chunks, and deleting it removes them all. Pages embedded before this change keep working; a page that was too long is chunked the next time it changes. (#​29218)

    A page is embedded as at most 100 chunks (about 200,000 tokens of body); past that, the rest of the body is found by keyword only. A page's name and description lead every chunk, up to 500 tokens. One semantic search looks at up to 1,000 distinct pages and notes (up to 10,000 vectors). A rewrite drops the page's old chunks by id after the new ones are stored; on a vector store that can't delete by id, old chunks stay but only ever point back to the same page.

  • Knowledge page and note writes now retry a failed embed or vector write a few times over about 10 seconds before giving up. That gives a cold scale-to-zero database (Neon, Lakebase) time to wake, so its pages aren't silently left out of semantic search. If every try fails, the write still succeeds and the warning names the page, for example Knowledge couldn't embed page "docs:acme#guides/cli" after retrying; it is findable by keyword search only until it is rewritten. Errors the embedding provider marks as not retryable, such as an input over its limit, are not retried. (#​29288)

  • Added read-only page-store readers to @mastra/core/knowledge for browsing UIs: listKnowledgeTreeChildren (one level of the page tree, child scopes first), readKnowledgePage (a page with its readable ancestors, notes, pin, referenced pages and notes that mention it), readKnowledgePageGraph (a bounded page graph with parent and mention edges, limits in DEFAULT_KNOWLEDGE_PAGE_GRAPH_LIMITS; mentions come from every note on a walked page until a graph-wide maxNotes budget runs out, which sets truncated), searchKnowledgeExplorerPages and readKnowledgePageActivity. Each takes a read scope ('all' for an owner view) and passes it to every store call, so pages and notes outside it never appear, mention edges included. They never write. Added readKnowledgeScopeGrants, which lists the declared grants onto the scope pages in a graph (principal scope, its page id when it has one, target and role), keeping only principals the read scope can read, and knowledge.scopeGrants(scope), which gives each principal the declared rules name on one scope with its strongest role. (#​28362)

  • Knowledge tool results now name each page by the ref knowledge_read accepts. Search, read and page results carry ref: the scope address for a scope page, the full key path for a nested page (docs:acme#guides/observability/logging), or the page id when a page on the path has no key. The old key field held only the last path segment, so agents that copied it got "not found" for nested pages; it is gone. A read that misses now lists up to three close matches by ref, and an ambiguous page name lists its candidates by ref. (#​29207)

  • Fixed pinned Knowledge listings stopping at 100 pages per level. A listing now reads every child of a page while its token budget has room, and only ends with "…and more" when the budget runs out. (#​29298)

  • GET /knowledge/:knowledgeKey/pages/:pageId also opens a page by the reference tools name it with: a scope address (site:mastra) or scope#key/path (site:mastra#topics/observability). It reads only what the caller can read, and an unreadable or unknown reference still returns 404. (#​29215)

  • Sped up readKnowledgePageGraph, which loads the Studio Knowledge graph, on large page trees. It now reads the tree a level at a time: one child-page count for the whole level, a listing only for pages that have children, one note count across the walked pages, and one lookup for linked pages outside the walk. A 300-page tree went from 1,221 sequential storage calls to 37, which matters most on remote databases such as Turso. The returned graph is unchanged. (#​29288)

  • Added an index of the [[links]] in Knowledge page bodies, so a host can count and list which pages link where without loading every page. countPageLinks and listPageLinks on the Knowledge store return in and out links for a set of pages, gated by the reader's scopes like every other read. Note [[mentions]] show up there as note links. (#​29260)

    How links resolve

    • Links resolve when a page is written through the Knowledge tools, upsertPage or removePage.
    • A link matches by page id, scope#key/key path, scope address or page name: the page's own scope first, then the whole store.
    • A link to a page that doesn't exist yet resolves once that page is created. It follows renames and moves, and goes back to unresolved when its target is deleted or merged away.
    • One write re-resolves at most 100 pages that link to what it changed. Past that it logs a warning, and the rest catch up when they are next written or when knowledge.reindexLinks() runs.

    Pages written before this release, or straight to the store, aren't indexed until they are written again. Run knowledge.reindexLinks() once to index them all; it needs no vector store.

    await knowledge.reindexLinks();
    const store = await mastra.getStorage()!.getStore('knowledge');
    const counts = await store!.countPageLinks({ pageIds: [page.id], read: 'all' });
    // { [page.id]: { in: 3, out: 1 } }
  • Added readKnowledgePageLinkCounts and readKnowledgePageLinks to @mastra/core/knowledge: exact link counts for any set of pages and one page's links with the pages at their other ends, read from the store's link index and filtered by the caller's read scope. Both return null when the store has no link index. (#​29260)

    import { readKnowledgePageLinkCounts, readKnowledgePageLinks } from '@mastra/core/knowledge';
    
    const counts = await readKnowledgePageLinkCounts(store, { pageIds: [pageId], read });
    if (counts === null) {
      // this store has no link index
    }
    const links = await readKnowledgePageLinks(store, { pageId, read, direction: 'out' });
  • Knowledge page paths ignore leading, trailing and doubled slashes, so docs:acme#models/gateways/ names the same page as docs:acme#models/gateways. This holds for upsertPage, removePage, the note methods, [[links]] in notes, page refs given to the Knowledge tools, and pinnedContext refs. Previously upsertPage refused such paths and reads reported the page as not found. (#​29207)

  • pinnedContext.maxTokens no longer has a 2,000-token ceiling. It budgets pinned pages only, so set it below your model's context limit to leave room for instructions, conversation history and model output, for example pinnedContext: { maxTokens: 8000 }. The default is still 500. (#​29257)

  • Fill pinned Knowledge listings level by level within their token budget. A listing with depth 2 or 3 now shows every child that fits first, then grandchildren, and so on, instead of stopping at a fixed 50 entries in sibling order. Each entry still prints under its parent, and a listing that is cut still ends with …and more; knowledge_list from: "<ref>". (#​29263)

  • The knowledge_read tool now tells the model that a read from a line runs until the page ends or the read's token cap is reached, and that limit caps notes, children and mentioning notes, not lines. Models had been passing limit expecting a set number of lines back. (#​29258)

  • Removed knowledge.reindex(). It re-embedded every page and note in the store, which is slow and costly on a large store. Pages and notes are embedded when they're written through the Knowledge tools, upsertPage or the note methods. If an embed fails, the write still succeeds and the content stays findable by keyword search; it's embedded again the next time it changes. upsertPage skips writes whose content is unchanged, so sending identical content again doesn't retry a failed embed; the Knowledge tools and updateNote embed on every write. (#​29256)

    If you called reindex() after loading content, delete the call and load the content through upsertPage instead, which embeds each new or changed page as it's written. There is no bulk re-embed for content that's already stored:

    // Before
    await knowledge.reindex();
    
    // After
    await knowledge.upsertPage({ scope: 'org:acme', path: 'refunds', name: 'Refund policy', body });
  • Fixed read-only Knowledge runs filling storage with empty scopes. A run that only searches, lists or reads Knowledge, and pinned-context delivery each turn, no longer creates the run's resource:, thread: or other held scopes. A held scope is created, with its template pages and its belongsTo placement, the first time a run writes Knowledge (knowledge_page, knowledge_note, knowledge_pin). (#​29288)

    Before, a Slack agent that only answered questions with knowledge_search created an empty resource:slack:<userId> and thread: scope for every user and conversation, which filled the Studio tree.

    A scope with no pages yet reads as not found until the first write creates it.

  • knowledge_search now finds pages that match any word of the query, not only pages that match every word. Pages with more of the query's words rank higher, so a long question still finds the page that holds most of its words. A page whose name contains the whole query still comes first. When several pages match the same number of words, a word in the name counts more than one in the description, and one in the description more than one in the body. A search uses the first 16 distinct words of the query; repeated words count once. (#​29207)

  • knowledge_search handles queries agents commonly write. Quoted words ("api" "responses") are searched as plain words; before, the quote marks were matched literally and nothing was found. A search that finds nothing now says so and suggests one to three key words or knowledge_list. Passing a page ref such as docs:acme#models as scope now explains that scope takes scope addresses, instead of reporting the page as not found. (#​29207)

    A first knowledge_read of a long page that stops early now also returns the outline of the headings it did not reach (up to about 500 tokens of them, with their line numbers and sizes), so an agent can read the section it needs next instead of paging toward it. Search results at their limit no longer tell the agent to search again; the default Knowledge instructions now say to follow more only when what the agent needs is not already in what it has read, and to answer once it has it.

    The first knowledge_read of a page too long to return whole now shows the whole page at a glance: every heading with its line number and the first lines under it, with ⋯ lines a–b where lines are left out, within the read cap. Read the part you need with section or from. A page with more headings than fit falls back to its top lines and the headings below them.

  • Knowledge tree child counts now say when they were capped. If a scope has more child scopes than the 1,000 the tree looks through for each level, the page gets childCountCapped: true and its childCount is a lower bound. The Knowledge explorer then shows the count as "1000+" in the tree and in the graph's "pages under" line, instead of a number that's too low. (#​29216)

  • Each page in a Knowledge tree level (GET /api/knowledge/:knowledgeKey/tree, listKnowledgeTreeChildren) now carries childCount: how many readable pages, and for a scope how many readable child scopes, sit directly under it. A browsing UI can show the count and offer to open only pages that have children. The count is left out when the storage adapter can't count children yet. (#​29216)

    const level = await client.getKnowledge('main').tree({ parentId: scopeId });
    level.pages.map(page => `${page.name} (${page.childCount ?? '?'})`);

    Storage adapters gain countChildPages({ parentIds, read }), which counts a batch of parents' child pages in one query (in-memory, LibSQL, PostgreSQL, MySQL, MongoDB).

  • Stored spans now use the service name from the Observability config when the span metadata does not set one. (#​29201)

  • Fixed getStepResult and getStepResultAsync returning null for the loop body inside .dowhile() and .dountil() conditions on the evented workflow engine. Loop conditions now see the body's latest result on every iteration, matching the default engine. (#​26999)

  • Fixed approving a tool loaded by ToolSearchProcessor after a server restart. The approved call failed with Tool "<name>" not found when memory no longer returned the search result. It now runs with the tools it had when the model made the call. Fixes #​29196. (#​29251)

  • Fixed run.cancel() leaving suspended nested workflow runs suspended after the parent was canceled. Nested runs, including foreach iterations, are now canceled with their parent on both the default and evented engines. Calling cancel() again on a run that is already canceled also cancels nested runs it left suspended. If storage fails while canceling nested runs, cancel() rejects and can be retried. (#​24780) (#​29170)

  • Fixed Property '#private' is missing when passing a voice provider such as new OpenAIRealtimeVoice() to an agent's voice option or to CompositeVoice (#​29016). (#​29074)

    These options now accept the IMastraVoice interface, exported from @mastra/core/voice. agent.voice and agent.getVoice() return IMastraVoice. If you annotate a variable with a voice type, use IMastraVoice instead of MastraVoice.

    import { Agent } from '@mastra/core/agent';
    import { OpenAIRealtimeVoice } from '@mastra/voice-openai-realtime';
    
    export const voiceAgent = new Agent({
      id: 'voice-agent',
      name: 'Voice Agent',
      instructions: 'You are a voice assistant.',
      model: 'openai/gpt-6.1-sol',
      voice: new OpenAIRealtimeVoice(), // failed with TS2322 before this fix
    });
  • Fixed signal-started runs to use the ifIdle.streamOptions.runId you provide. This applies when sendSignal, sendMessage, queueSignal or queueMessage wakes an idle thread or queues a run behind the active one. The id appears in accepted.runId, output.runId and thread events. If the id already names a known run, accepted rejects. Calls without an id still get a random one. (#​29152)

    const { accepted } = agent.sendSignal(
      { type: 'user-message', contents: 'hello' },
      { resourceId, threadId, ifIdle: { streamOptions: { runId: 'my-run-id' } } },
    );
    await accepted; // { action: 'wake', runId: 'my-run-id', output }
  • Fixed progress from tools wrapped with createStep from @mastra/core/workflows/evented never reaching the workflow stream. The tool now receives the step writer, the run actor (including the actor step option) and suspendPayload, the same as with createStep from @mastra/core/workflows. Fixes #​29164. (#​29165)

  • Fixed controller tool status for validation failures and explicit error results, including delegated tool results. Successful results containing business error fields retain their successful status. (#​28812)

  • Fixed directly resumed nested workflows in the evented engine so they continue suspended parent runs. (#​25979)

  • Fixed type declarations for bundled AI SDK packages. When @mastra/core bundled two versions of the same AI SDK package, one version's types overwrote the other's, so types such as LanguageModelV3 used by the bundled OpenAI, Google and Anthropic providers resolved to any. Each version now keeps its own copy, so these types are checked again. Fixes #​28732. (#​29171)

  • Fixed retained workflow handles and abort listeners after durable agent runs settle (#​29009) (#​29065)

  • Fixed dataset experiments that target a workflow so their traces carry the experiment ID. Workflow runs and every auto-resume now include experimentId in tracing metadata, so Studio's experiment page shows total cost, tokens and run duration for workflow experiments, the same as it does for agent experiments. (#​29245)

[v1.81.0](https://redirect.

[!IMPORTANT]
✂ PR body was truncated to here.


Configuration

📅 Schedule: (in timezone America/Los_Angeles)

  • Branch creation
    • Monday through Friday (* * * * 1-5)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about these updates again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate
renovate Bot force-pushed the renovate/mastra-core-1.x branch 2 times, most recently from 6de8b46 to b37e28c Compare April 18, 2026 08:52
@renovate
renovate Bot force-pushed the renovate/mastra-core-1.x branch 4 times, most recently from 89500b2 to c6c04dd Compare May 2, 2026 01:49
@renovate
renovate Bot force-pushed the renovate/mastra-core-1.x branch 2 times, most recently from 0098570 to 19b0b67 Compare May 12, 2026 09:49
@renovate
renovate Bot force-pushed the renovate/mastra-core-1.x branch 6 times, most recently from 8819cf7 to ce69b78 Compare May 23, 2026 00:33
@renovate
renovate Bot force-pushed the renovate/mastra-core-1.x branch 7 times, most recently from 1c3a41f to f14940c Compare June 2, 2026 05:51
@renovate
renovate Bot force-pushed the renovate/mastra-core-1.x branch 5 times, most recently from 0d1eb5f to 00dbc1f Compare June 12, 2026 23:30
@renovate
renovate Bot force-pushed the renovate/mastra-core-1.x branch 2 times, most recently from d53662f to 734056f Compare June 20, 2026 08:38
@renovate
renovate Bot force-pushed the renovate/mastra-core-1.x branch 2 times, most recently from f8bb9dd to 9002d5d Compare June 27, 2026 06:35
@renovate
renovate Bot force-pushed the renovate/mastra-core-1.x branch 4 times, most recently from f08ce8a to dd69ca4 Compare August 2, 2026 04:48
@renovate
renovate Bot force-pushed the renovate/mastra-core-1.x branch 4 times, most recently from debf13c to 3bd730d Compare August 14, 2026 22:57
@renovate
renovate Bot force-pushed the renovate/mastra-core-1.x branch 3 times, most recently from 2dd4033 to f93fd4f Compare August 22, 2026 05:16
@renovate
renovate Bot force-pushed the renovate/mastra-core-1.x branch 4 times, most recently from 1fdaecc to f5f5c3a Compare August 30, 2026 02:50
@renovate
renovate Bot force-pushed the renovate/mastra-core-1.x branch 5 times, most recently from c26bc51 to 47c8779 Compare September 6, 2026 05:51
@renovate
renovate Bot force-pushed the renovate/mastra-core-1.x branch 5 times, most recently from e39294d to 751c543 Compare September 13, 2026 18:00
@renovate
renovate Bot force-pushed the renovate/mastra-core-1.x branch 2 times, most recently from 62106e0 to d1db72a Compare September 18, 2026 21:56
@renovate
renovate Bot force-pushed the renovate/mastra-core-1.x branch 2 times, most recently from 568e8dc to 141978e Compare September 25, 2026 18:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants