#24280·mastra

Delegated sub-agent's suspend answer gets overwritten by the delegation call's own later completion

Author: tomfrenkenCreated Sep 17, 2026Updated Sep 17, 2026
LabelsAgentsimpact:loweffort:mediumstatus: auto-triagedstatus: needs approval

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:

  1. The data-tool-call-suspended UI part a client renders as a question/approval card, and
  2. The native tool-<name> UI part a client answers via addToolOutput({ 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

  1. A supervisor agent delegates to a sub-agent (agents: { subAgent }).
  2. The sub-agent's own tool calls context.agent.suspend(...) (no requireApproval, a plain suspend/resume tool — e.g. asking the user a question).
  3. Client renders the question from the data-tool-call-suspended part and answers via addToolOutput({ toolCallId: <delegation call id>, output: <answer> }).
  4. Backend resumes via agent.resumeStream(resumeData, { runId, toolCallId }).
  5. The sub-agent finishes; the delegation call's own tool-result chunk lands for that same toolCallId, overwriting the native part's output with the delegation's own return value.
  6. Any client-side check like "is messageParts output-available for this toolCallId" now finds a differently-shaped output and 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.