support tool approvals for pydantic-ai chatbot#9621
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
2 issues found across 10 files
Architecture diagram
sequenceDiagram
participant UI as Chat UI (Frontend)
participant Utils as Chat Logic (utils.ts)
participant Srv as Marimo Backend (LLM Impl)
participant Agent as Pydantic-AI Agent
Note over UI,Agent: Phase 1: Tool Request with Approval Gate
UI->>Srv: Send User Message
Srv->>Srv: CHANGED: Initialize VercelAIAdapter with AI_SDK_VERSION
Srv->>Agent: run_stream()
Agent->>Agent: Model selects tool with requires_approval=True
Agent-->>Srv: Yield DeferredToolRequest
Srv-->>UI: NEW: Stream 'tool-approval-request' chunk
Note over UI,Agent: Phase 2: User Approval & Auto-Resumption
UI->>UI: Render Approve/Deny buttons
UI->>Utils: User clicks "Approve"
Utils->>Utils: NEW: Update part state to 'approval-responded'
rect rgb(240, 240, 240)
Note right of Utils: Auto-send trigger
Utils->>UI: CHANGED: hasPendingToolCalls() returns true
UI->>Srv: NEW: Automatic POST with approval metadata
end
Note over UI,Agent: Phase 3: Tool Execution
Srv->>Srv: NEW: sanitize_part() preserves 'approval' fields
Srv->>Agent: Resume stream with response parts
alt User Approved
Agent->>Agent: Call tool function
Agent-->>Srv: Yield tool-output
else User Denied
Agent-->>Srv: Yield tool-error/denied
end
Srv-->>UI: Stream final assistant response
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
Pull request overview
This PR extends marimo’s pydantic-ai / Vercel AI SDK bridge to support tool approval flows end-to-end, ensuring approval request/response payloads are preserved across frontend ↔ backend round-trips.
Changes:
- Passes the Vercel AI SDK version through the pydantic-ai adapter and preserves tool approval payloads by preferring raw wire
partswhen rebuilding UI messages. - Refactors tool-part sanitization into a public helper and updates the frontend auto-send logic to resume automatically after tool calls / approval responses.
- Adds regression tests (Python + frontend) and updates the pydantic-ai chat example to demonstrate approval-gated tools and richer SDK parts.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
marimo/_ai/llm/_impl.py |
Ensures pydantic-ai adapter uses sdk_version and rebuilds UI messages from raw/dumped parts with sanitization. |
marimo/_ai/_types.py |
Adds raw part snapshotting + round-trip helpers to prevent dropping unmodeled SDK fields (e.g., approval). |
marimo/_ai/_pydantic_ai_utils.py |
Promotes part sanitization helper to public API and applies it during message conversion. |
frontend/src/plugins/impl/chat/chat-ui.tsx |
Wires tool approval response handler into message rendering and enables SDK-driven auto-send. |
frontend/src/components/chat/chat-utils.ts |
Replaces custom auto-send heuristics with ai@6 helpers and exposes hasPendingToolCalls. |
frontend/src/components/chat/__tests__/chat-utils.test.ts |
Adds unit tests covering auto-send behavior for tool/approval states and trailing parts. |
tests/_ai/llm/test_impl.py |
Adds pydantic-ai integration regressions for approval request emission and approval payload preservation. |
tests/_ai/test_ai_types.py |
Adds round-trip tests for raw parts snapshotting and equality semantics. |
tests/_ai/test_pydantic_utils.py |
Updates tests to use the renamed sanitize_part helper. |
examples/ai/chat/pydantic-ai-chat.py |
Enhances the example to demonstrate approval-gated tools and full SDK part showcase. |
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- VercelAIAdapter: fallback when sdk_version kwarg unsupported - chat-ui: only pass addToolApprovalResponse to the last message - ChatMessage: move _raw_parts off the msgspec struct so it isn't serialized Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- ChatMessage: snapshot raw dicts per-element so unmodeled SDK fields (e.g. `approval`, `callProviderMetadata`) survive even when `parts` mixes typed and dict entries. - test_impl: don't pin approvalId to a specific value; older pydantic-ai emits a UUID while newer reuses toolCallId. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 3 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| ) | ||
| for part in message.parts | ||
| ], | ||
| [sanitize_part(p) for p in message.raw_or_dumped_parts()], |
There was a problem hiding this comment.
we don't want to remove None values anymore?
There was a problem hiding this comment.
With the new sanitize_part function, we only filter fields that pydantic-ai accepts, so None will get removed naturally. Thanks though! I did not write the description well.
| ) | ||
| for part in message.parts | ||
| ], | ||
| [sanitize_part(p) for p in message.raw_or_dumped_parts()], |
There was a problem hiding this comment.
Codex review highlighted this:
sanitize_part()builds its allowlist from pydantic field aliases only, so typed marimo dataclass dumps likeToolInvocationPart(...)lose snake_case keys such astool_call_idbefore pydantic-ai can validate them. For example, anoutput-availabletool part is filtered down without eithertoolCallIdortool_call_id, causingUIMessage(...)validation to fail for chat histories containing typed marimo tool parts. This path should either preserve field names in the allowlist or convert dataclass dumps to the frontend alias form before sanitizing.
I'm not sure about implementations, so leave this to your discretion.
There was a problem hiding this comment.
I don't think users can hit this issue, but will monitor and try out
5d11746
into
sham/fix-pydantic-ai-chat-interruption
📝 Summary
Supports more AI sdk parts, refactors logic to be more maintainable.
callProviderMetadata,providerExecuted,preliminary) live on AI SDK parts that marimo's typed dataclasses don't model. The message now snapshots the raw wire payload per-part in _raw_parts, so those fields survive every round-trippydantic_ai._build_ui_messagesuses the raw payload. It now callsmessage.raw_or_dumped_parts()so the approval/tool state the frontend just sent us makes it into the agent run unmodified. The oldasdict+_remove_none_valuespath was lossy.sanitize_partstrips keys the AI SDK's { ...part, state, ... } spread can leak from prior tool states (e.g. a stale output clinging to an approval-requested part).📋 Pre-Review Checklist
✅ Merge Checklist