Add missing Vercel AI SDK v6 tool approval part types#4388
Add missing Vercel AI SDK v6 tool approval part types#4388DouweM merged 25 commits intopydantic:mainfrom
Conversation
|
I used #4390 to help reproduce the error using the AI SDK client from Node.js and then test the fix. I'll get that covering the full integration and clean it up shortly. I recognize that it's somewhat odd to be calling a Node.js test suite from Pytest. We understand if you're hesitant to merge this. But regardless, it'll be helpful to me, versus clicking through an AI Elements frontend. |
|
Hi Ben, Have been testing this out today. For example, if we deny a tool use and the agent re-tries the tool I am seeing the following stack trace: What seems to be happening is that on a tool denial, the agent seems to want to re-try the same tool (at least sometimes) and this so far makes sense. When I put a breakpoint In _adapter when we call VercelAIAdapter.deferred_tool_results on line 172 where we do in self.run_input.messages[1] I see is the following: (please ignore the get_current_date tool call, don't know why the agent decided to do that) So we see a StepStartUIPart, a ToolOutputDeniedPart (first denial), StepStartUIPart, ToolApprovalRespondedPart(second denial we just sent) as parts and both denials match So we have deferred_tool_results containing both denials by their id. This seems to then be an issue for further validations. The immediate simplest fix to me seems to be in iter_tool_approval_responses to specifically filter for only ToolApprovalRespondedPart and DynamicToolApprovalRespondedPart respectively (this seems to work locally), but I am not 100% convinced this will not cause further issues. The other option is probably to ensure that a UI treats a denial as a end of the message and a subsequent request for the same tool as a completely new message, so there two are processed separately. I am not sure if this is the intended behaviour though. |
|
Thanks, I will look into this either tomorrow or Tuesday.
In theory the denial reason could contain a user message and so this would be true but I need to spend more time looking into how AI SDK is expecting these on the frontend. |
|
@dmmihov just pushed a change
It does exactly this and adjusts the unit tests to match. I repro'd this in #4390 as well. I should have time tomorrow to test this end to end. But in the mean time please do try it locally if you have time! |
|
Thank you, Ben. I will test this out tonight. |
|
Awesome, thank you! I am reasonably confident in this specific fix. I also have plans later this week to properly integrate these new features into a production application. So to the extent there are any other edge cases or subtleties I'll be looking closely. Definitely planning to address the issue the display issue for approval state as well: |
2f7df41 to
e67bde2
Compare
|
Fixed the uncovered built-in tool branch, should be ready to go now |
|
@bendrucker Please have a look at the merge conflicts; I just merged #4196 |
| timestamp: datetime = field(default_factory=_now_utc) | ||
| """The timestamp, when the tool returned.""" | ||
|
|
||
| status: Literal['success', 'error', 'denied'] = 'success' |
There was a problem hiding this comment.
I like the outcome name from the comment better than status!
| type=tool_name, | ||
| tool_call_id=part.tool_call_id, | ||
| input=_safe_args_as_dict(part), | ||
| state='output-denied', |
There was a problem hiding this comment.
This is implied by the class right? We shouldn't need to set it explicitly
| ) | ||
| else: | ||
| ui_parts.append( | ||
| ToolOutputAvailablePart( |
There was a problem hiding this comment.
We can also handle ToolOutputErrorPart here now right?
tests/test_vercel_ai.py
Outdated
| should be extracted. | ||
| """ | ||
| from pydantic_ai.ui.vercel_ai._utils import iter_tool_approval_responses | ||
| from pydantic_ai.ui.vercel_ai.request_types import DynamicToolOutputDeniedPart |
There was a problem hiding this comment.
Imports at the top of the file please
| elif isinstance(part, ToolOutputErrorPart): # pragma: no branch | ||
| output = {'error_text': part.error_text, 'is_error': True} | ||
| if isinstance(part, ToolOutputErrorPart): | ||
| output: Any = {'error_text': part.error_text, 'is_error': True} |
There was a problem hiding this comment.
We can just store error_text as output now, right?
|
Not a contributer yet, so can't push to this branch. |
|
Thanks @dmmihov, merged, will have another look at this tonight if I have time |
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… errors
- Use ToolReturnPart(outcome='error') instead of RetryPromptPart in
load_messages for tool errors from Vercel AI format
- Store builtin tool error_text directly as content instead of
{'error_text': ..., 'is_error': True} wrapper
- Handle outcome='error' in dump path, emitting ToolOutputErrorPart
- Add backward-compat is_error content check for old serialized data
- Handle ToolReturnPart(outcome='error') in event stream
- Extract _dump_tool_call_part helper to reduce C901 complexity
- Remove redundant state= assignments implied by class defaults
- Move inline test imports to top of file
|
/gemini Summary |
DouweM
left a comment
There was a problem hiding this comment.
@bendrucker Thanks Ben, just a small final change and we're good to go!
…al default - Rename 'error' → 'failed' in BaseToolReturnPart.outcome to align with the upcoming ToolFailed exception (pydantic#2586) - Use ToolDenied().message instead of hardcoded string in _denial_reason() for consistency
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Douwe Maan <douwe@pydantic.dev> Co-authored-by: Darin Mihov <dmmihov@gmail.com> Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Douwe Maan <douwe@pydantic.dev> Co-authored-by: Darin Mihov <dmmihov@gmail.com> Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>

Changes
ToolApprovalRequestedPart,ToolApprovalRespondedPart,ToolOutputDeniedPart, and their dynamic counterparts torequest_types.pyso the adapter can load and dump all AI SDK v6 tool approval states (approval-requested,approval-responded,output-denied){'is_denied': True}inBaseToolReturnPart.metadata, exposed via anis_deniedproperty. This is UI display state (not semantic content sent to the model), ensuring denied tools render correctly after dump/load cyclesis_deniedin the streaming path (_event_stream.py) to emitToolOutputDeniedChunkdirectly from the part, replacing the previous_denied_tool_idsside-channel that scanned message historyTesting
output-deniedparts with and without an approval reasonToolOutputDeniedChunkemission for denied dynamic tools via the full agent flowReferences
build_run_inputfailing on deferred tool approval/denial. #4387Pre-Review Checklist
make formatandmake typecheck.Pre-Merge Checklist