Repository: mastra-ai/mastra
File: packages/core/src/loop/workflows/agentic-execution/tool-call-step.ts (TypeScript)

Code before:
              {
                resumeLabel: inputData.toolCallId,
              },
            );
          } else {
            // Remove approval metadata since we're resuming (either approved or declined)
            await removeToolMetadata(inputData.toolName, 'approval');

            if (!resumeData.approved) {
              return {
                result: 'Tool call was not approved by the user',
                ...inputData,
              };
            }
          }
        } else if (isResumeToolCall) {
          await removeToolMetadata(inputData.toolName, 'suspension');
        }

        //this is to avoid passing resume data to the tool if it's not needed
        // For agent tools, always pass resume data so the agent tool wrapper knows to call
        // resumeStream instead of stream (otherwise the sub-agent restarts from scratch)
        const isAgentTool = inputData.toolName?.startsWith('agent-');
        const isWorkflowTool = inputData.toolName?.startsWith('workflow-');
        const resumeDataToPassToToolOptions =
          !(isAgentTool || isWorkflowTool) && toolRequiresApproval && Object.keys(resumeData).length === 1 && 'approved' in resumeData
            ? undefined
            : resumeData;

        const toolOptions: MastraToolInvocationOptions = {
          abortSignal: options?.abortSignal,
          toolCallId: inputData.toolCallId,
          // Pass all messages (input + response + memory) so sub-agents (agent-* tools) receive
          // the full conversation context and can make better decisions. Each sub-agent invocation
          // uses a fresh unique thread, so storing this context in that thread is scoped and safe.
          messages: isAgentTool ? messageList.get.all.aiV5.model() : messageList.get.input.aiV5.model(),
          outputWriter,
          // Pass current step span as parent for tool call spans
          tracingContext: modelSpanTracker?.getTracingContext(),
          // Pass workspace from _internal (set by llmExecutionStep via prepareStep/processInputStep)
          workspace: _internal?.stepWorkspace,
          // Forward requestContext so tools receive values set by the workflow step
          requestContext,
          suspend: async (suspendPayload: any, options?: SuspendOptions) => {
            if (options?.requireToolApproval) {
              controller.enqueue({
                type: 'tool-call-approval',
                runId,
                from: ChunkFrom.AGENT,
                payload: {
                  toolCallId: inputData.toolCallId,

Review comment (bug):
oh this actually isn't needed. coderabbit was wrong about that.
The agent tool (subagent) might have gotten suspended because it has some tool in it that requires approval, that's why we have to make sure to confirm that the tool isn't a subagent, to ensure the resumeData { approved: true/false } is sent down to the subagent.

This does not apply to workflows

Code after:
              {
                resumeLabel: inputData.toolCallId,
              },
            );
          } else {
            // Remove approval metadata since we're resuming (either approved or declined)
            await removeToolMetadata(inputData.toolName, 'approval');

            if (!resumeData.approved) {
              return {
                result: 'Tool call was not approved by the user',
                ...inputData,
              };
            }
          }
        } else if (isResumeToolCall) {
          await removeToolMetadata(inputData.toolName, 'suspension');
        }

        //this is to avoid passing resume data to the tool if it's not needed
        // For agent tools, always pass resume data so the agent tool wrapper knows to call
        // resumeStream instead of stream (otherwise the sub-agent restarts from scratch)
        const isAgentTool = inputData.toolName?.startsWith('agent-');
        const isWorkflowTool = inputData.toolName?.startsWith('workflow-');
        const resumeDataToPassToToolOptions =
          !isAgentTool && toolRequiresApproval && Object.keys(resumeData).length === 1 && 'approved' in resumeData
            ? undefined
            : resumeData;

        const toolOptions: MastraToolInvocationOptions = {
          abortSignal: options?.abortSignal,
          toolCallId: inputData.toolCallId,
          // Pass all messages (input + response + memory) so sub-agents (agent-* tools) receive
          // the full conversation context and can make better decisions. Each sub-agent invocation
          // uses a fresh unique thread, so storing this context in that thread is scoped and safe.
          messages: isAgentTool ? messageList.get.all.aiV5.model() : messageList.get.input.aiV5.model(),
          outputWriter,
          // Pass current step span as parent for tool call spans
          tracingContext: modelSpanTracker?.getTracingContext(),
          // Pass workspace from _internal (set by llmExecutionStep via prepareStep/processInputStep)
          workspace: _internal?.stepWorkspace,
          // Forward requestContext so tools receive values set by the workflow step
          requestContext,
          suspend: async (suspendPayload: any, options?: SuspendOptions) => {
            if (options?.requireToolApproval) {
              controller.enqueue({
                type: 'tool-call-approval',
                runId,
                from: ChunkFrom.AGENT,
                payload: {
                  toolCallId: inputData.toolCallId,