Skip to content

Comments

Add tool approval integration for Vercel AI adapter#3772

Merged
DouweM merged 49 commits intopydantic:mainfrom
bendrucker:vercel-ai-tool-approval
Feb 18, 2026
Merged

Add tool approval integration for Vercel AI adapter#3772
DouweM merged 49 commits intopydantic:mainfrom
bendrucker:vercel-ai-tool-approval

Conversation

@bendrucker
Copy link
Contributor

@bendrucker bendrucker commented Dec 19, 2025

Closes #4279

Adds tool approval integration for the Vercel AI adapter, enabling human-in-the-loop workflows with AI SDK UI.

Summary

  • Setting sdk_version=6 enables tool approval
  • Emit tool-approval-request chunks for deferred tool approvals
  • Emit tool-output-denied chunks when user denies tool execution
  • Auto-extract approval responses from follow-up requests
  • Add approval field to tool parts with ToolApprovalRequested/ToolApprovalResponded states

Usage

from pydantic_ai import Agent
from pydantic_ai.ui.vercel_ai import VercelAIAdapter

agent = Agent('openai:gpt-5.2')

@agent.tool_plain(requires_approval=True)
def delete_file(path: str) -> str:
    """Delete a file from the filesystem."""
    os.remove(path)
    return f'Deleted {path}'

@app.post('/chat')
async def chat(request: Request) -> Response:
    adapter = await VercelAIAdapter.from_request(request, agent=agent, sdk_version=6)
    return adapter.streaming_response(adapter.run_stream())

When sdk_version=6, the adapter will:

  1. Emit tool-approval-request chunks when tools with requires_approval=True are called
  2. Automatically extract approval responses from follow-up requests
  3. Emit tool-output-denied chunks for rejected tools, passing the denial reason through as ToolDenied

AI SDK Tool Approval Protocol

Tool approval is an AI SDK v6 feature. Here's how the protocol works:

Protocol Flow

sequenceDiagram
    participant Model
    participant Server
    participant Client

    Model->>Server: tool call
    Server->>Client: tool-input-start
    Server->>Client: tool-input-available
    Server->>Client: tool-approval-request

    Note over Client: User approves/denies

    Client->>Server: approval response (next request)

    alt Approved
        Server->>Server: Execute tool
        Server->>Client: tool-output-available
    else Denied
        Server->>Client: tool-output-denied
        Server->>Model: denial info
    end
Loading

Chunk Types

tool-approval-request

Server → client:

{
  "type": "tool-approval-request",
  "approvalId": "<uuid>",
  "toolCallId": "<tool-call-id>"
}

tool-output-denied

Server → client, when denied:

{
  "type": "tool-output-denied",
  "toolCallId": "<tool-call-id>"
}

Why the Server Emits This Chunk

"Pydantic AI doesn't have an event for denials because denials never originate in Pydantic AI itself, they come from the user in the form of DeferredToolResults passed into agent.run" — #3760 comment

This is correct—the denial decision originates from the user via the ToolApprovalResponded field in tool parts. However, the AI SDK protocol expects the server to emit tool-output-denied for two reasons:

  1. The client needs confirmation that the tool lifecycle is complete. When the server receives a denial and emits tool-output-denied, the client can transition its UI from "awaiting result" to "denied".

  2. Just as the server emits tool-output-available when a tool executes successfully, it emits tool-output-denied when execution is skipped due to denial. This gives the client a consistent signal for each tool call's final state.

The flow is:

  1. Client sends denial via approval: { id, approved: false, reason } in the tool part
  2. Server extracts this from deferred_tool_results and passes it to the agent
  3. When processing the denied tool result, server emits tool-output-denied to the client
  4. Client updates UI to show the tool was denied

Tool Part Approval States

Tool parts include an approval field tracking the approval lifecycle:

Awaiting Response

{ "id": "<approval-id>" }

User Responded

{ "id": "<approval-id>", "approved": true/false, "reason": "optional" }

Two-Step Flow

Unlike regular tool calls, approved tools require two model interactions:

  1. Model requests tool → server returns with tool-approval-request → awaits user
  2. User decision sent → server executes (if approved) or informs model (if denied)

Changes

  • Add ToolApprovalRequested/ToolApprovalResponded types and approval field to tool UI parts
  • Add ToolApprovalRequestChunk and ToolOutputDeniedChunk response chunks to event stream
  • Gate tool approval behavior on sdk_version=6 (added in Fix compatibility with Vercel AI SDK v5 by adding SDK version param #4166), enabling auto-extraction of approval responses via deferred_tool_results cached property
  • Pass denial reason through as ToolDenied when provided
  • Add tool approval section to Vercel AI docs

Testing

  • Approval request/denied chunk emission, opt-out behavior, and approval extraction (approved, denied with reason, no approval)
  • Snapshot updates for approval field on tool parts

References

@bendrucker bendrucker force-pushed the vercel-ai-tool-approval branch 2 times, most recently from 099f07a to 1160591 Compare December 19, 2025 05:03
@bendrucker bendrucker marked this pull request as ready for review December 19, 2025 05:27
@bendrucker
Copy link
Contributor Author

Thanks for the quick reviews! Will get the test issue fixed and reply to your comments shortly.

@bendrucker bendrucker force-pushed the vercel-ai-tool-approval branch 2 times, most recently from 2deeb25 to 9eebd80 Compare December 20, 2025 11:03
@github-actions
Copy link
Contributor

This PR is stale, and will be closed in 3 days if no reply is received.

@github-actions github-actions bot added the Stale label Dec 30, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Jan 3, 2026

Closing this PR as it has been inactive for 10 days.

@github-actions github-actions bot closed this Jan 3, 2026
@bendrucker
Copy link
Contributor Author

Have been on a break for a bit, will get the comments addressed early this week if not today.

@DouweM DouweM removed the Stale label Jan 5, 2026
@DouweM
Copy link
Collaborator

DouweM commented Jan 5, 2026

@bendrucker Thanks Ben, I should probably have disabled the stale bot before the holidays :)

@DouweM DouweM reopened this Jan 5, 2026
@DouweM DouweM added feature New feature request, or PR implementing a feature (enhancement) size: M Medium PR (101-500 weighted lines) labels Jan 6, 2026
@bendrucker
Copy link
Contributor Author

Thanks for your patience! Made the requested changes and provided a reference to relevant AI SDK test snapshots for the behavioral question.

@bendrucker
Copy link
Contributor Author

Squashing another upstream cause of a test flake: pytest-dev/pytest-xdist#1299

@DouweM
Copy link
Collaborator

DouweM commented Jan 23, 2026

@bendrucker I merged some other changes for the Vercel AI event stream that had been in the works for a few weeks -- can you resolve the conflicts please?

@bendrucker
Copy link
Contributor Author

Resolved!

@DouweM
Copy link
Collaborator

DouweM commented Jan 30, 2026

@bendrucker Please have a look at the conflicts

github-actions[bot]

This comment was marked as resolved.

github-actions[bot]

This comment was marked as resolved.

github-actions[bot]

This comment was marked as resolved.

github-actions[bot]

This comment was marked as resolved.

The override was silently dropping **kwargs instead of forwarding
them to the constructor, which could surprise subclasses.
Only document the new sdk_version parameter and refer to the base
class for all other parameters, since this override exists purely
for IDE autocomplete.
Imports only needed for dispatch_request/from_request method
signatures are now lazy, keeping the runtime import footprint light.
addToolResult is deprecated in AI SDK v6 in favor of
addToolApprovalResponse for the tool approval flow.
@DouweM
Copy link
Collaborator

DouweM commented Feb 13, 2026

@bendrucker Did you mean to push your changes?

@bendrucker
Copy link
Contributor Author

Whoops, done!

github-actions[bot]

This comment was marked as resolved.

@DouweM
Copy link
Collaborator

DouweM commented Feb 13, 2026

@bendrucker We've got merge conflicts 😅

# Conflicts:
#	docs/ui/vercel-ai.md
#	pydantic_ai_slim/pydantic_ai/ui/vercel_ai/_event_stream.py
#	tests/test_vercel_ai.py
@bendrucker
Copy link
Contributor Author

No worries, saw em, fixing them now!

@DouweM DouweM merged commit bbebf55 into pydantic:main Feb 18, 2026
32 checks passed
Copy link
Collaborator

DouweM commented Feb 18, 2026

@bendrucker Thanks Ben! It took a minute but I'll get this out in a release later today :)

@bendrucker
Copy link
Contributor Author

Thank you! Appreciate your patience, Python is still somewhat new to me. Learned a bunch and will be eager to integrate this soon!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting author revision feature New feature request, or PR implementing a feature (enhancement) size: L Large PR (501-1500 weighted lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Vercel AI integration: ToolApprovalRequestChunk not emitted for DeferredToolRequests, approval responses not parseable

3 participants