Skip to content

fix(ai-sdk): support AI SDK v6 native tool approval flow#15345

Open
TheIsrael1 wants to merge 4 commits intomainfrom
sudsy-mare
Open

fix(ai-sdk): support AI SDK v6 native tool approval flow#15345
TheIsrael1 wants to merge 4 commits intomainfrom
sudsy-mare

Conversation

@TheIsrael1
Copy link
Copy Markdown
Contributor

@TheIsrael1 TheIsrael1 commented Apr 14, 2026

Fixes #14818 and #15268.

Two related problems with tool call approvals in AI SDK v6:

1. handleChatStream didn't detect native approve() calls

When the client uses AI SDK v6's approve() method, the SDK re-submits the conversation with an approval-responded part in the messages. handleChatStream was treating this as a normal chat turn and calling stream() instead of resumeStream().

Now handleChatStream scans the incoming messages for approval-responded parts, extracts the runId from the composite approvalId ("${runId}::${toolCallId}"), and routes to resumeStream automatically — no extra wiring needed in user code.

2. v6 stream wasn't emitting native tool-approval-request parts

convertMastraChunkToAISDKv6 was routing tool-call-approval through the base converter which only produced data-tool-call-approval. AI SDK v6's useChat needs a tool-approval-request part to wire up approve() on the client side.

Now for v6, both are emitted: tool-approval-request (for native approve() support) and data-tool-call-approval (backwards compat with @mastra/react hooks that read runId from the data chunk).

If you're on v6 and using useChat, tool approvals now just work without needing to manually post to /approve-tool-call.

ELI5

This PR fixes the approval flow so that when a user clicks the built‑in "approve" button in AI SDK v6 chat, the conversation correctly resumes and the SDK emits the right approval messages—no extra server wiring required.

Overview

Fixes tool-call approval handling in AI SDK v6 by:

  • Detecting native approve() re-submissions and routing them to resumeStream.
  • Emitting AI SDK v6 native approval parts so useChat/approve() wiring works, while retaining legacy data chunks for backward compatibility.

Key Changes

  • Native V6 approval detection

    • Added extractV6NativeApproval(messages) in client-sdks/ai-sdk/src/chat-route.ts which inspects the last trailing assistant message for a tool part with state === 'approval-responded', parses a composite approvalId ("${runId}::${toolCallId}") to recover runId, and returns { resumeData, runId } or null.
    • Updated handleChatStream to use extractV6NativeApproval (when version === 'v6' and no explicit resumeData) and route to agentObj.resumeStream(...) instead of agentObj.stream(...) when appropriate; base options use the recovered runId.
  • Dual-emission of approval parts (v6 + legacy)

    • Added APPROVAL_ID_SEPARATOR = '::' constant in client-sdks/ai-sdk/src/helpers.ts for composing/parsing approvalId.
    • convertMastraChunkToAISDKv6 now returns two parts for tool-call-approval: a v6 ToolApprovalRequest (tool-approval-request with computed approvalId) and the legacy data-tool-call-approval DataChunkType (runId, toolCallId, toolName, args, resumeSchema) to preserve backwards compatibility.
    • Output types updated to allow convertMastraChunkToAISDK to return an array of parts.
  • Stream conversion & transformer updates

    • createAgentStreamToAISDKTransformer and createWorkflowStreamToAISDKTransformer updated to handle arrays returned from conversion functions by enqueueing each element individually (skip falsy items), enabling multiple parts emitted from a single Mastra chunk to be processed correctly.
  • Tests

    • Added comprehensive tests in client-sdks/ai-sdk/src/tests/tool-call-approval.test.ts:
      • Unit tests for extractV6NativeApproval (parsing, runId extraction, reason handling, edge cases).
      • Control-flow tests ensuring handleChatStream calls resumeStream (not stream) when approval-responded parts are present.
      • Conversion and integration tests asserting v6 output contains both tool-approval-request and data-tool-call-approval and that UI message stream reflects approval-requested state.
    • Adjusted helpers/transformer tests to cover array-return conversion behavior.

Impact

- convertMastraChunkToAISDKv6 now emits tool-approval-request (v6 native)
  alongside data-tool-call-approval (backwards compat) for tool-call-approval chunks
- handleChatStream auto-detects AI SDK v6 approve() submissions and routes
  to resumeStream instead of stream
- approvalId is encoded as "${runId}::${toolCallId}" so the server recovers
  the runId without a DB lookup
- Uses lastIndexOf for safe separator parsing

Fixes #14818
Fixes #15268

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
mastra-docs-1.x Ready Ready Preview, Comment Apr 14, 2026 2:07pm

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 14, 2026

🦋 Changeset detected

Latest commit: fd384fd

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@mastra/ai-sdk Patch
@internal/playground Patch
mastra Patch
create-mastra Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 14, 2026

Walkthrough

Emits v6 native tool-approval-request alongside legacy data-tool-call-approval, adds extractV6NativeApproval to parse approval-responded parts and derive resume data, and updates handleChatStream to call agent.resumeStream when appropriate. Adds transformers and tests to support array-form conversions; documentation changes only for release notes.

Changes

Cohort / File(s) Summary
Changeset Documentation
.changeset/seven-suns-dress.md, .changeset/tired-comics-cover.md
Added patch notes documenting v6 approval routing/resume behavior and fix for requireApproval with handleChatStream; describes emission of native tool-approval-request while preserving legacy data-tool-call-approval.
Tool Approval Extraction & Routing
client-sdks/ai-sdk/src/chat-route.ts
Added and exported extractV6NativeApproval(messages) to locate approval-responded tool parts and build { runId, resumeData }; updated v6 handleChatStream to use extracted effective runId/resumeData and call agent.resumeStream when appropriate.
Conversion Types & Dual Emission
client-sdks/ai-sdk/src/helpers.ts
Introduced APPROVAL_ID_SEPARATOR, extended output types to include tool-approval-request, changed convertMastraChunkToAISDKv6 to return either a single part or an array (emitting both a v6 tool-approval-request and legacy data-tool-call-approval), and added mapping for tool-approval-request → UI message part.
Stream Transformers
client-sdks/ai-sdk/src/transformers.ts
Refactored workflow/agent transformers to accept conversion results that may be arrays or single items; added helper to enqueue each transformed part, filter falsy results, and preserve prior routing for tool/workflow/network parts.
Tests
client-sdks/ai-sdk/src/__tests__/tool-call-approval.test.ts
Reworked tests to use shared fixtures (createApprovalStream, collectChunks); added unit tests for extractV6NativeApproval, approve/resume control-flow tests for handleChatStream, conversion tests validating dual emission for v5/v6, and UI message stream integration assertions.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title is concise, descriptive, uses imperative mood, and clearly summarizes the main fix for v6 native tool approval flow support.
Linked Issues check ✅ Passed All coding requirements from linked issues are met: extractV6NativeApproval detects native approve() calls [#14818], convertMastraChunkToAISDKv6 now emits both tool-approval-request and data-tool-call-approval [#14818], handleChatStream routes to resumeStream for approvals [#14818].
Out of Scope Changes check ✅ Passed All changes are directly related to fixing tool approval flow: extractV6NativeApproval helper, handleChatStream routing logic, tool-approval-request emission, approvalId parsing, and transformer array handling for consistency.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sudsy-mare

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.changeset/seven-suns-dress.md:
- Line 5: The changeset text references backwards compatibility with
`@mastra/react` which is not in the frontmatter; update the entry so it only
describes effects for `@mastra/ai-sdk` (e.g., “handleChatStream now routes to
resumeStream when client.approve() is used, and the v6 stream emits native
tool-approval-request parts in addition to data-tool-call-approval for
`@mastra/ai-sdk` consumers”) or create a separate changeset describing the
`@mastra/react` change; ensure references to handleChatStream, resumeStream,
approve(), tool-approval-request and data-tool-call-approval remain accurate but
do not mention packages not listed in the frontmatter.

In `@client-sdks/ai-sdk/src/chat-route.ts`:
- Around line 33-58: extractV6NativeApproval currently scans the entire
conversation and can pick up old "approval-responded" parts; change it to only
inspect the trailing assistant turn that approve() re-submits: find the last
message with message.role === 'assistant' (from the end), then iterate that
single message.parts from the end and return the resumeData/runId when
encountering a part where isToolUIPart(part) && part.state ===
'approval-responded'; this prevents handleChatStream/resumeStream from resuming
on historical approvals.

In `@client-sdks/ai-sdk/src/transformers.ts`:
- Around line 281-327: transformWorkflow currently assumes
convertMastraChunkToAISDK returns a single part and thus drops additional parts
from v6 chunks; update the workflow transformer to accept and handle an array of
parts from convertMastraChunkToAISDK (or flatten its result) so each returned
part is converted/enqueued (e.g., in the workflow-step-output handling inside
transformWorkflow, iterate over the array returned by convertMastraChunkToAISDK
and call the same conversion/enqueue logic for each element), ensuring
tool-call-approval v6 chunks produce both tool-approval-request and the legacy
data chunk; reference transformWorkflow and convertMastraChunkToAISDK in your
changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f27d3a41-28fd-42e6-9269-733f94925b60

📥 Commits

Reviewing files that changed from the base of the PR and between 7a483fe and ea96643.

📒 Files selected for processing (6)
  • .changeset/seven-suns-dress.md
  • .changeset/tired-comics-cover.md
  • client-sdks/ai-sdk/src/__tests__/tool-call-approval.test.ts
  • client-sdks/ai-sdk/src/chat-route.ts
  • client-sdks/ai-sdk/src/helpers.ts
  • client-sdks/ai-sdk/src/transformers.ts

- extractV6NativeApproval now only inspects the last trailing assistant
  message so approval-responded parts from earlier turns are never
  re-processed
- transformWorkflow workflow-step-output case now handles array returns
  from convertMastraChunkToAISDK (triggered by tool-call-approval v6
  chunks); update both callers to enqueue each element of the array
- changeset: drop reference to @mastra/react which is not in frontmatter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@TheIsrael1 TheIsrael1 changed the title fix(ai-sdk): native v6 tool-approval-request and approve() resume flow fix(ai-sdk): support AI SDK v6 native tool approval flow Apr 14, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client-sdks/ai-sdk/src/transformers.ts`:
- Around line 740-754: The branch handling Array.isArray(part) can return nested
arrays because convertFullStreamChunkToUIMessageStream may expand a single part
into multiple UI chunks; change the map(...).filter(Boolean) pipeline to produce
a flattened array (e.g., use flatMap to call
convertFullStreamChunkToUIMessageStream for each p and flatten the results, then
filter falsy values) so the returned value contains only UI chunks (no inner
arrays). Keep the same arguments and onError callback (safeParseErrorObject)
when calling convertFullStreamChunkToUIMessageStream and ensure the final result
type matches the surrounding convertMastraChunkToAISDK / transformers.ts
expectations.
- Around line 287-328: The code assumes
convertFullStreamChunkToUIMessageStream(...) and transformNetwork(...) return
single chunks, but they may return arrays; update enqueueTransformedPart to
flatten any array-returning helpers: if transformedChunk is an array, iterate
over each element and apply the same branching logic (check .type and call
transformAgent, transformWorkflow, transformNetwork or controller.enqueue for
each), and similarly if transformNetwork(...) returns an array iterate and
enqueue each item rather than enqueuing the array itself; ensure you reuse the
existing branches for 'tool-agent', 'tool-workflow', 'tool-network' so behavior
stays consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 84e426bd-e5af-4c8a-8861-cb2eaeebd758

📥 Commits

Reviewing files that changed from the base of the PR and between ea96643 and fd384fd.

📒 Files selected for processing (3)
  • .changeset/seven-suns-dress.md
  • client-sdks/ai-sdk/src/chat-route.ts
  • client-sdks/ai-sdk/src/transformers.ts
✅ Files skipped from review due to trivial changes (1)
  • .changeset/seven-suns-dress.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • client-sdks/ai-sdk/src/chat-route.ts

…sformedPart

transformNetwork returns TransformNetworkResult[] in the
network-execution-event-step-finish case; previously the array was
passed directly to controller.enqueue. Now treated consistently with
the workflow branch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel vercel bot temporarily deployed to Preview – mastra-docs-1.x April 14, 2026 16:28 Inactive
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
client-sdks/ai-sdk/src/transformers.ts (2)

287-336: ⚠️ Potential issue | 🟠 Major

Flatten convertFullStreamChunkToUIMessageStream results before branching on .type.

enqueueTransformedPart still assumes a single transformed chunk. If the converter returns an array, it gets enqueued as one invalid chunk payload instead of fan-out processing.

Suggested fix
       const enqueueTransformedPart = (p: any) => {
-        const transformedChunk = convertFullStreamChunkToUIMessageStream<any>({
+        const transformedChunk = convertFullStreamChunkToUIMessageStream<any>({
           part: p as any,
           sendReasoning,
           sendSources,
           messageMetadataValue: p ? messageMetadata?.({ part: p as TextStreamPart<ToolSet> }) : undefined,
           sendStart,
@@
-        if (transformedChunk) {
-          if (transformedChunk.type === 'tool-agent') {
-            const payload = transformedChunk.payload;
+        const transformedChunks = Array.isArray(transformedChunk)
+          ? transformedChunk
+          : transformedChunk
+            ? [transformedChunk]
+            : [];
+
+        for (const chunk of transformedChunks) {
+          if (chunk.type === 'tool-agent') {
+            const payload = chunk.payload;
             const agentTransformed = transformAgent<OUTPUT>(payload, bufferedSteps);
             if (agentTransformed) controller.enqueue(agentTransformed);
-          } else if (transformedChunk.type === 'tool-workflow') {
-            const payload = transformedChunk.payload;
+          } else if (chunk.type === 'tool-workflow') {
+            const payload = chunk.payload;
             const workflowChunk = transformWorkflow(
               payload,
               bufferedSteps,
@@
-          } else if (transformedChunk.type === 'tool-network') {
-            const payload = transformedChunk.payload;
+          } else if (chunk.type === 'tool-network') {
+            const payload = chunk.payload;
             const networkChunk = transformNetwork(payload, bufferedSteps, true);
             if (Array.isArray(networkChunk)) {
               for (const c of networkChunk) {
                 if (c) controller.enqueue(c);
               }
             } else if (networkChunk) {
               controller.enqueue(networkChunk);
             }
           } else {
-            controller.enqueue(transformedChunk as any);
+            controller.enqueue(chunk as any);
           }
         }
-        }
       };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client-sdks/ai-sdk/src/transformers.ts` around lines 287 - 336,
enqueueTransformedPart assumes convertFullStreamChunkToUIMessageStream returns a
single object but it may return an array; change the logic to first
normalize/flatten the result of convertFullStreamChunkToUIMessageStream (when
called in enqueueTransformedPart) into an array (e.g., wrap non-array into a
single-element array), then iterate each transformed item and perform the
existing branching on item.type (handling 'tool-agent' via transformAgent,
'tool-workflow' via transformWorkflow, 'tool-network' via transformNetwork, and
the default case) enqueuing each resulting message via controller.enqueue;
ensure onError handling and existing parameters (sendReasoning, sendSources,
messageMetadataValue, sendStart, sendFinish, responseMessageId) are preserved
when creating/processing each item.

746-760: ⚠️ Potential issue | 🟠 Major

map(...).filter(Boolean) still returns nested arrays in the array-part branch.

When convertFullStreamChunkToUIMessageStream(...) returns arrays, this code returns Array<(chunk | chunk[])> and inner arrays leak to downstream enqueue paths.

Suggested fix
         if (Array.isArray(part)) {
-          return part
-            .map(p =>
-              convertFullStreamChunkToUIMessageStream({
-                part: p as any,
-                sendReasoning: streamOptions?.sendReasoning,
-                sendSources: streamOptions?.sendSources,
-                onError(error) {
-                  return safeParseErrorObject(error);
-                },
-              }),
-            )
-            .filter(Boolean);
+          return part.flatMap(p => {
+            const transformed = convertFullStreamChunkToUIMessageStream({
+              part: p as any,
+              sendReasoning: streamOptions?.sendReasoning,
+              sendSources: streamOptions?.sendSources,
+              onError(error) {
+                return safeParseErrorObject(error);
+              },
+            });
+
+            return Array.isArray(transformed) ? transformed.filter(Boolean) : transformed ? [transformed] : [];
+          });
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client-sdks/ai-sdk/src/transformers.ts` around lines 746 - 760, The branch
handling Array.isArray(part) can produce nested arrays because
convertFullStreamChunkToUIMessageStream may return arrays; update this branch to
flatten the results (e.g., use flatMap or map + flat) so it returns a
single-level Array of chunks before filtering. Locate the mapping that calls
convertFullStreamChunkToUIMessageStream (the block that passes part: p,
sendReasoning/sendSources, onError) and replace the map(...).filter(Boolean)
pattern with a flattening approach and then filter(Boolean) to ensure downstream
enqueue paths receive only non-nested chunk items.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@client-sdks/ai-sdk/src/transformers.ts`:
- Around line 287-336: enqueueTransformedPart assumes
convertFullStreamChunkToUIMessageStream returns a single object but it may
return an array; change the logic to first normalize/flatten the result of
convertFullStreamChunkToUIMessageStream (when called in enqueueTransformedPart)
into an array (e.g., wrap non-array into a single-element array), then iterate
each transformed item and perform the existing branching on item.type (handling
'tool-agent' via transformAgent, 'tool-workflow' via transformWorkflow,
'tool-network' via transformNetwork, and the default case) enqueuing each
resulting message via controller.enqueue; ensure onError handling and existing
parameters (sendReasoning, sendSources, messageMetadataValue, sendStart,
sendFinish, responseMessageId) are preserved when creating/processing each item.
- Around line 746-760: The branch handling Array.isArray(part) can produce
nested arrays because convertFullStreamChunkToUIMessageStream may return arrays;
update this branch to flatten the results (e.g., use flatMap or map + flat) so
it returns a single-level Array of chunks before filtering. Locate the mapping
that calls convertFullStreamChunkToUIMessageStream (the block that passes part:
p, sendReasoning/sendSources, onError) and replace the map(...).filter(Boolean)
pattern with a flattening approach and then filter(Boolean) to ensure downstream
enqueue paths receive only non-nested chunk items.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e0c19ae5-d52c-45ae-86da-5fc1cb3178fc

📥 Commits

Reviewing files that changed from the base of the PR and between fd384fd and 2e0a5da.

📒 Files selected for processing (1)
  • client-sdks/ai-sdk/src/transformers.ts

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.

requireApproval resume doesn't work with handleChatStream + AssistantChatTransport (AI SDK v6)

2 participants