Fix image output validators, unknown tool retries, incomplete tool calls, and parallel task cancellation#4325
Fix image output validators, unknown tool retries, incomplete tool calls, and parallel task cancellation#4325dsfaccini wants to merge 12 commits intopydantic:mainfrom
Conversation
…lls, and parallel task cancellation - Run output validators on image outputs (both streaming and non-streaming) - Use per-tool retry tracking for unknown tool calls instead of global counter - Detect and raise clear IncompleteToolCall error on truncated model responses - Cancel all sibling tasks on any exception, not just CancelledError Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
This PR bundles 4 separate, unrelated changes together (image output validators, unknown tool retries, incomplete tool calls, parallel task cancellation) with no linked GitHub issues for any of them. This makes it harder to review, discuss, and (if needed) revert individual changes independently. Per the project's contribution guidelines, each of these would benefit from its own issue with clearly defined scope and maintainer input, and its own PR. This is especially true for the behavioral changes to unknown tool retry tracking and the new I'd recommend splitting this into 4 separate PRs, each linked to its own issue, so maintainers can evaluate each change on its own merits. I've left inline comments on the specific changes below in case you proceed with this PR as-is. @DouweM |
…4385) - Deduplicate output validator execution between text/image response handlers - Remove proactive _check_incomplete_tool_calls (truncated responses now retry) - Fix ctx.retry for output validators on tool path to use global counter - Differentiate output tools vs function tools in _call_tool RunContext Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolve conflicts with upstream refactoring: - _agent_graph.py: keep both our dedup helpers and main's _emit_skipped_output_tool; inline _check_incomplete_tool_calls into increment_retries (matching main); add increment_retries call for unknown tools (matching main) - _tool_manager.py: apply output-tool retry differentiation to main's _build_tool_context method - test_agent.py: keep main's tests, update error expectations for unknown tools - Update test snapshots in test_streaming, test_ui, test_vercel_ai Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Closes #4385
Summary
1. Image output validators were being skipped (bug fix)
Files:
_agent_graph.py,result.pyWhen you configure an agent with
output_type=BinaryImage, you can register output validators — functions that run on the output before it's returned to you. These validators were never actually called for image outputs. Both the non-streaming and streaming paths now loop through validators, just like they already do for text and tool-based outputs.2. Unknown tool calls were exhausting retries prematurely (bug fix)
File:
_agent_graph.pyWhen a model returns a tool call for a tool that doesn't exist, pydantic-ai tells the model "that tool doesn't exist" and retries. The old code incremented a global retry counter for unknown tools before processing any valid tools in the same response. So if the model returned both an unknown tool AND a valid tool together, the global counter could hit the limit and fail the whole run. Unknown tools now use per-tool retry tracking instead.
3. Deduplicate output validator execution + fix consistent
ctx.retryfor output validators (#4385)Files:
_agent_graph.py,_tool_manager.pyOutput validator logic was duplicated between
_handle_text_responseand_handle_image_response. Extracted shared helpers (_build_output_run_context,_run_output_validators).Additionally,
@agent.output_validatorreceived inconsistentctx.retryvalues: text/image paths used the global retry counter, but the tool output path used per-tool retry count. Now output tools consistently use the global output retry counter in the RunContext passed to validators.4. Truncated tool calls now retry instead of failing immediately
File:
_agent_graph.pyRemoved the proactive
_check_incomplete_tool_callscall fromprocess_tool_calls. When a model's response gets truncated (finish_reason == 'length'), incomplete tool call args now go through normal validation/retry flow instead of immediately raisingIncompleteToolCall. The helper is still used inincrement_retriesto provide a clear error message when max retries are exceeded.5. Parallel tool tasks weren't cancelled on non-cancellation errors (bug fix)
File:
_agent_graph.pyWhen pydantic-ai runs multiple tools in parallel and one fails, it should cancel the others. The
exceptblock only caughtasyncio.CancelledError, so regular exceptions (likeRuntimeError) left sibling tasks running as orphans. Changed toexcept BaseExceptionso any exception triggers cancellation.🤖 Generated with Claude Code