You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
background-task-started lifecycle chunk is dropped before reaching the UI stream in handleChatStream
Summary
First off — thank you for the background tasks feature, it's a genuinely great
fit for "fire and keep chatting" UX. We're running into one issue that currently
makes it hard to consume the task lifecycle from a web-chat UI.
When an agent dispatches a tool as a background task, Mastra emits a structured background-task-started chunk on the agent stream carrying the real taskId
({ taskId, toolName, toolCallId }). In a standard web-chat setup that routes
through @mastra/ai-sdk's handleChatStream() → agent.stream() → toAISdkStream(), this chunk appears to be dropped on two independent paths
and never reaches the UI. As a result consumers cannot obtain the taskId from
the documented lifecycle event, and are forced to regex-parse it out of the
tool-result string instead.
This is meant as a constructive report — happy to refine it or provide more
repro detail if that helps.
Why we need taskId (real-world impact)
Background tasks are explicitly designed for the "fire and keep chatting" UX:
the agent dispatches a long-running tool, returns an immediate acknowledgement,
and the user keeps talking in the same thread while the task runs. To make that
UX actually work, the frontend needs the taskId at dispatch time for several
concrete things — all of which are currently blocked without the chunk:
Render a live progress card at dispatch time. We show a "task running"
card the moment the task is dispatched so the user sees something is
happening and can keep chatting. Without taskId, the card cannot be created
until the task finishes — which for a multi-minute research task defeats the
entire point of running it in the background.
Subscribe to the task's lifecycle out-of-band. Because the agent's stream
returns immediately after dispatch (it does not block on the task), the only
way for the UI to observe running → output → completed/failed is to open a
separate subscription scoped to the task — e.g. backgroundTaskManager.stream({ taskId })
/ getTask(taskId) over a dedicated SSE endpoint. That subscription is keyed
on taskId; no taskId, no way to follow the task.
Show streamed output / the final report. Our deep-research task streams
its output incrementally and writes a report on completion. Both arrive via
the task-scoped manager stream as background-task-output / background-task-completed, which the UI can only receive once it has the taskId.
Render a terminal state instead of an infinite spinner. When a background
task fails, the failure is reported as background-task-failed on the
manager stream. Without a taskId-driven subscription, the UI never learns
the task died and the card spins forever.
In short: taskId is the handle that makes a background task observable from
the UI at all. The whole value proposition of background tasks (non-blocking,
keep-chatting, live progress, eventual result) depends on the frontend getting
that handle at dispatch time — which is exactly what background-task-started
was meant to provide.
Versions
@mastra/core: 1.42.0
@mastra/ai-sdk: 1.4.5
ai: 6.0.188
Evidence
1. Mastra does emit the chunk
In the agent's tool-execution loop, when a background task is dispatched
(fallbackToSync === false), the background-task-started chunk is enqueued
onto the stream controller with a structured payload, and the tool returns the
human-readable acknowledgement as its result:
const{ task, fallbackToSync }=awaitbgTask.dispatch();if(!fallbackToSync){safeEnqueue(controller,{type: "background-task-started",
runId,from: "AGENT",payload: {taskId: task.id,toolName: inputData.toolName,toolCallId: inputData.toolCallId,},});return{result: `Background task started. Task ID: ${task.id}. The tool "${inputData.toolName}" is running in the background. You will be notified when it completes.`,
...inputData,};}
So the chunk exists, carries exactly the structured { taskId, toolName, toolCallId }
a UI needs, and is placed onto the agent stream controller.
2. Path A — onChunk is not invoked for it
In the agent loop, the onChunk option is only invoked for a fixed allowlist of
chunk types:
default:
safeEnqueue(controller,chunk);// ← chunk IS enqueued to the stream...}if(["text-delta","reasoning-delta","source","tool-call","tool-call-input-streaming-start","tool-call-delta","tool-call-input-streaming-end","raw",].includes(chunk.type)){if(chunk.type==="raw"&&!includeRawChunks){continue;}awaitoptions?.onChunk?.(chunk);// ← ...but onChunk is NOT called for background-task-started}
So a consumer passing onChunk to handleChatStream / agent.stream()
expecting to observe background-task-started never receives it.
3. Path B — toAISdkStream transformer discards it
@mastra/ai-sdk's convertFullStreamChunkToUIMessageStream returns null for
any chunk type it doesn't explicitly handle and that isn't data- prefixed:
default: {if(isDataChunkType(payload)){// ... pass through data-* chunks}returnnull;// ← background-task-started falls through here and is dropped}
There's no handling of any background-task-* chunk type anywhere in @mastra/ai-sdk (a grep for background-task returns no matches in the
package), so the chunk — even though present on the agent fullStream — is
filtered out before reaching the AI SDK UI message stream.
Current (fragile) workaround
Because the chunk is unreachable, we're currently extracting the taskId by
regex-matching the tool-result string:
// tool-result is a plain string, set by Mastra at dispatch time:// "Background task started. Task ID: <id>. The tool \"...\" is running in the background."constmatch=output.match(/Backgroundtaskstarted\.\s+TaskID:\s+([^\s.]+)/i);consttaskId=match?.[1]?.trim();
This works today but is brittle — it depends on a natural-language string and
its exact wording, spacing, and punctuation, and it throws away the structured
payload the chunk already carries. It would be great to not have to rely on it.
Expected behavior
One (or both) of these would resolve it cleanly:
@mastra/core: include background-task-started (and ideally the other
agent-stream background-task chunks) in the onChunk allowlist so the
callback fires for them.
@mastra/ai-sdk: have toAISdkStream / convertFullStreamChunkToUIMessageStream
pass background-task-* chunks through (e.g. as data- data parts) so they
survive into the UI message stream the AI SDK UI consumes.
Either change would let a web-chat consumer obtain the structured { taskId, toolName, toolCallId } from the lifecycle event directly, instead of
regex-parsing the tool-result string.
Reproduction
import{handleChatStream}from"@mastra/ai-sdk";// 1. Configure an agent with backgroundTasks.tools enabled for some tool,// and backgroundTasks.enabled on the Mastra instance.// 2. Route a web chat through handleChatStream (version: "v6").// 3. Trigger a prompt that makes the model call the background-eligible tool.conststream=awaithandleChatStream({
mastra,
agentId,version: "v6",params: { messages,memory: {thread: tId,resource: rId}},defaultOptions: {onChunk: (chunk)=>{console.log("chunk:",chunk.type);// You'll see tool-call, tool-result, text-delta, reasoning-delta...// ...but never "background-task-started", even though the task was// dispatched and is visible via backgroundTaskManager.getTask(taskId).},},});
Querying mastra_background_tasks (or backgroundTaskManager.getTask) confirms
the task was created and started, so dispatch definitely happened — the chunk
simply never propagates to the UI layer.
Additional context
The manager API backgroundTaskManager.stream() does emit background-task-running
/ -completed / etc., which we already use for the post-dispatch lifecycle.
But those arrive on a long-lived stream that stays open until the caller's AbortSignal fires, and running fires when the task starts executing (not
when it's enqueued). Wiring that into a request-scoped web-chat stream
introduces timing/ordering complexity that background-task-started on the
agent stream was presumably meant to avoid.
The embedded background-tasks doc (docs-agents-background-tasks.md) lists background-task-started as an agent-stream chunk type with payload { taskId, toolName, toolCallId }, which reads as though it's meant to be
consumable from the agent's own stream.
background-task-startedlifecycle chunk is dropped before reaching the UI stream inhandleChatStreamSummary
First off — thank you for the background tasks feature, it's a genuinely great
fit for "fire and keep chatting" UX. We're running into one issue that currently
makes it hard to consume the task lifecycle from a web-chat UI.
When an agent dispatches a tool as a background task, Mastra emits a structured
background-task-startedchunk on the agent stream carrying the realtaskId(
{ taskId, toolName, toolCallId }). In a standard web-chat setup that routesthrough
@mastra/ai-sdk'shandleChatStream()→agent.stream()→toAISdkStream(), this chunk appears to be dropped on two independent pathsand never reaches the UI. As a result consumers cannot obtain the
taskIdfromthe documented lifecycle event, and are forced to regex-parse it out of the
tool-result string instead.
This is meant as a constructive report — happy to refine it or provide more
repro detail if that helps.
Why we need
taskId(real-world impact)Background tasks are explicitly designed for the "fire and keep chatting" UX:
the agent dispatches a long-running tool, returns an immediate acknowledgement,
and the user keeps talking in the same thread while the task runs. To make that
UX actually work, the frontend needs the
taskIdat dispatch time for severalconcrete things — all of which are currently blocked without the chunk:
Render a live progress card at dispatch time. We show a "task running"
card the moment the task is dispatched so the user sees something is
happening and can keep chatting. Without
taskId, the card cannot be createduntil the task finishes — which for a multi-minute research task defeats the
entire point of running it in the background.
Subscribe to the task's lifecycle out-of-band. Because the agent's stream
returns immediately after dispatch (it does not block on the task), the only
way for the UI to observe
running → output → completed/failedis to open aseparate subscription scoped to the task — e.g.
backgroundTaskManager.stream({ taskId })/
getTask(taskId)over a dedicated SSE endpoint. That subscription is keyedon
taskId; notaskId, no way to follow the task.Show streamed output / the final report. Our deep-research task streams
its output incrementally and writes a report on completion. Both arrive via
the task-scoped manager stream as
background-task-output/background-task-completed, which the UI can only receive once it has thetaskId.Render a terminal state instead of an infinite spinner. When a background
task fails, the failure is reported as
background-task-failedon themanager stream. Without a
taskId-driven subscription, the UI never learnsthe task died and the card spins forever.
In short:
taskIdis the handle that makes a background task observable fromthe UI at all. The whole value proposition of background tasks (non-blocking,
keep-chatting, live progress, eventual result) depends on the frontend getting
that handle at dispatch time — which is exactly what
background-task-startedwas meant to provide.
Versions
@mastra/core:1.42.0@mastra/ai-sdk:1.4.5ai:6.0.188Evidence
1. Mastra does emit the chunk
In the agent's tool-execution loop, when a background task is dispatched
(
fallbackToSync === false), thebackground-task-startedchunk is enqueuedonto the stream controller with a structured payload, and the tool returns the
human-readable acknowledgement as its result:
So the chunk exists, carries exactly the structured
{ taskId, toolName, toolCallId }a UI needs, and is placed onto the agent stream controller.
2. Path A —
onChunkis not invoked for itIn the agent loop, the
onChunkoption is only invoked for a fixed allowlist ofchunk types:
So a consumer passing
onChunktohandleChatStream/agent.stream()expecting to observe
background-task-startednever receives it.3. Path B —
toAISdkStreamtransformer discards it@mastra/ai-sdk'sconvertFullStreamChunkToUIMessageStreamreturnsnullforany chunk type it doesn't explicitly handle and that isn't
data-prefixed:There's no handling of any
background-task-*chunk type anywhere in@mastra/ai-sdk(a grep forbackground-taskreturns no matches in thepackage), so the chunk — even though present on the agent
fullStream— isfiltered out before reaching the AI SDK UI message stream.
Current (fragile) workaround
Because the chunk is unreachable, we're currently extracting the
taskIdbyregex-matching the tool-result string:
This works today but is brittle — it depends on a natural-language string and
its exact wording, spacing, and punctuation, and it throws away the structured
payload the chunk already carries. It would be great to not have to rely on it.
Expected behavior
One (or both) of these would resolve it cleanly:
@mastra/core: includebackground-task-started(and ideally the otheragent-stream background-task chunks) in the
onChunkallowlist so thecallback fires for them.
@mastra/ai-sdk: havetoAISdkStream/convertFullStreamChunkToUIMessageStreampass
background-task-*chunks through (e.g. asdata-data parts) so theysurvive into the UI message stream the AI SDK UI consumes.
Either change would let a web-chat consumer obtain the structured
{ taskId, toolName, toolCallId }from the lifecycle event directly, instead ofregex-parsing the tool-result string.
Reproduction
Querying
mastra_background_tasks(orbackgroundTaskManager.getTask) confirmsthe task was created and started, so dispatch definitely happened — the chunk
simply never propagates to the UI layer.
Additional context
backgroundTaskManager.stream()does emitbackground-task-running/
-completed/ etc., which we already use for the post-dispatch lifecycle.But those arrive on a long-lived stream that stays open until the caller's
AbortSignalfires, andrunningfires when the task starts executing (notwhen it's enqueued). Wiring that into a request-scoped web-chat stream
introduces timing/ordering complexity that
background-task-startedon theagent stream was presumably meant to avoid.
docs-agents-background-tasks.md) listsbackground-task-startedas an agent-stream chunk type with payload{ taskId, toolName, toolCallId }, which reads as though it's meant to beconsumable from the agent's own stream.
backgroundconfig is silently ignored — createTool constructor never reads opts.background #18451 (tool-levelbackgroundconfig silently ignored) and Background-dispatched sub-agent delegations send null tool-message content (provider 500) #17798 (background sub-agent delegations send
null tool-message content).
Thanks again for the great work on Mastra, and please let me know if there's
anything I can clarify or test on our side. 🙏