Skip to content

support tool approvals for pydantic-ai chatbot#9621

Merged
Light2Dark merged 10 commits into
sham/fix-pydantic-ai-chat-interruptionfrom
sham/pydantic-chat-tool-approvals
May 22, 2026
Merged

support tool approvals for pydantic-ai chatbot#9621
Light2Dark merged 10 commits into
sham/fix-pydantic-ai-chat-interruptionfrom
sham/pydantic-chat-tool-approvals

Conversation

@Light2Dark
Copy link
Copy Markdown
Collaborator

@Light2Dark Light2Dark commented May 20, 2026

📝 Summary

Supports more AI sdk parts, refactors logic to be more maintainable.

  • ChatMessage no longer loses unmodeled SDK fields. Approvals (and e.g. 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-trip
  • pydantic_ai._build_ui_messages uses the raw payload. It now calls message.raw_or_dumped_parts() so the approval/tool state the frontend just sent us makes it into the agent run unmodified. The old asdict + _remove_none_values path was lossy.
  • sanitize_part strips 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).
  • hasPendingToolCalls (frontend) rewritten. The old predicate treated "every tool ready & no trailing text" silently looped whenever an assistant message ended in a non-text part (file, source-url, data-*, reasoning) after a completed tool call. The fix uses some native AI sdk logic and some testing.
image

📋 Pre-Review Checklist

  • For large changes, or changes that affect the public API: this change was discussed or approved through an issue, on Discord, or the community discussions (Please provide a link if applicable).
  • Any AI generated code has been reviewed line-by-line by the human PR author, who stands by it.
  • Video or media evidence is provided for any visual changes (optional).

✅ Merge Checklist

  • I have read the contributor guidelines.
  • Documentation has been updated where applicable, including docstrings for API changes.
  • Tests have been added for the changes made.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 20, 2026

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

Project Deployment Actions Updated (UTC)
marimo-docs Ready Ready Preview, Comment May 21, 2026 3:29am

Request Review

@Light2Dark Light2Dark marked this pull request as ready for review May 20, 2026 07:56
@Light2Dark Light2Dark added the enhancement New feature or request label May 20, 2026
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

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
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread marimo/_ai/llm/_impl.py Outdated
Comment thread frontend/src/plugins/impl/chat/chat-ui.tsx
@Light2Dark Light2Dark requested a review from Copilot May 20, 2026 08:11
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 parts when 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.

Comment thread marimo/_ai/_types.py Outdated
Comment thread marimo/_ai/_pydantic_ai_utils.py
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>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Comment thread marimo/_ai/_types.py Outdated
Comment thread marimo/_ai/_pydantic_ai_utils.py
- 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>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

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

Comment thread marimo/_ai/_convert.py
@Light2Dark Light2Dark marked this pull request as ready for review May 21, 2026 03:17
@Light2Dark Light2Dark requested a review from Copilot May 21, 2026 03:17
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Comment thread marimo/_ai/_types.py
Comment thread marimo/_ai/llm/_impl.py
Comment thread marimo/_ai/llm/_impl.py
)
for part in message.parts
],
[sanitize_part(p) for p in message.raw_or_dumped_parts()],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we don't want to remove None values anymore?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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.

Comment thread marimo/_ai/llm/_impl.py
)
for part in message.parts
],
[sanitize_part(p) for p in message.raw_or_dumped_parts()],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Codex review highlighted this:

sanitize_part() builds its allowlist from pydantic field aliases only, so typed marimo dataclass dumps like ToolInvocationPart(...) lose snake_case keys such as tool_call_id before pydantic-ai can validate them. For example, an output-available tool part is filtered down without either toolCallId or tool_call_id, causing UIMessage(...) 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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I don't think users can hit this issue, but will monitor and try out

@Light2Dark Light2Dark merged commit 5d11746 into sham/fix-pydantic-ai-chat-interruption May 22, 2026
52 checks passed
@Light2Dark Light2Dark deleted the sham/pydantic-chat-tool-approvals branch May 22, 2026 03:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants