Delegated sub-agent's suspend answer gets overwritten by the delegation call's own later completion
Description
When a sub-agent's own tool calls context.agent.suspend() (e.g. an ask-user-question-style tool asking the user something), the delegation wrapper the supervisor calls (agent-<subAgentId>) forwards that suspend as its own suspension — reusing its own toolCallId for both:
- The
data-tool-call-suspendedUI part a client renders as a question/approval card, and - The native
tool-<name>UI part a client answers viaaddToolOutput({ toolCallId, output }).
Once the sub-agent later finishes for real, the delegation call also genuinely completes, and Mastra emits its own real tool-result for that same toolCallId — carrying { text, subAgentThreadId, subAgentResourceId, subAgentToolResults }, not the user's answer. The AI SDK's stream reducer (updateToolPart) unconditionally overwrites the native part's output with this new value, silently clobbering whatever the client had written there when the user answered.
There is no way for a client to tell "this toolCallId is still the original suspend, awaiting resolution" apart from "this toolCallId now belongs to the delegation call's own real completion" — both events reuse the exact same id.
Reproduction
- A supervisor agent delegates to a sub-agent (
agents: { subAgent }). - The sub-agent's own tool calls
context.agent.suspend(...)(norequireApproval, a plain suspend/resume tool — e.g. asking the user a question). - Client renders the question from the
data-tool-call-suspendedpart and answers viaaddToolOutput({ toolCallId: <delegation call id>, output: <answer> }). - Backend resumes via
agent.resumeStream(resumeData, { runId, toolCallId }). - The sub-agent finishes; the delegation call's own
tool-resultchunk lands for that sametoolCallId, overwriting the native part'soutputwith the delegation's own return value. - Any client-side check like "is
messagePartsoutput-available for this toolCallId" now finds a differently-shapedoutputand can no longer tell the question was actually answered.
I verified this end-to-end with a scripted MockLanguageModelV2 reproduction (supervisor → sub-agent → plain suspend() → resume → sub-agent finishes → delegation call's own real tool-result lands on the same toolCallId, overwriting the answer).
Expected behavior
A client should have a stable, collision-free way to know a suspended tool call was actually answered, independent of whatever unrelated completion later lands on the same toolCallId.
Fix
I've opened a PR that emits a second, live tool-call-suspended chunk marked resumed: true once a suspended delegation call actually resumes — mirroring the existing storage-only resumed marker already written to persisted messages (removeToolMetadata in tool-call-step.ts). It shares the same toolCallId, so its data-tool-call-suspended UI part collapses onto the original suspension client-side (data parts with the same (type, id) replace in place), giving a UI an authoritative "already resolved" signal that survives the later native-part overwrite.
Every internal consumer that watches for tool-call-suspended chunks (stream status tracking in stream/base/output.ts, the sub-agent/network forwarding loops in agent.ts and loop/network/index.ts, and the agent controller in session-run-engine.ts) needed a matching guard so this ack isn't mistaken for a fresh suspension.
Source: mastra-ai/mastra