feat: add RepairToolOrphans capability for fixing broken tool pairing

Closed
RI
7mo ago
+593-0

Summary

Implements the RepairToolOrphans capability (as requested by @DouweM in #4728) to repair broken tool-call/return pairing in persisted message history.

Previous PR #5021 was closed because it was implemented as a history_processor instead of a capability. This PR follows the guidance correctly.

What it does

  • Dangling tool calls: ToolCallPart with no matching return → injects synthetic ToolReturnPart in the next request
  • Dangling builtin calls: BuiltinToolCallPart without return → injects BuiltinToolReturnPart in the same ModelResponse (required by Anthropic)
  • Orphaned returns: ToolReturnPart/RetryPromptPart with no matching call → removed
  • Empty requests: If cleanup empties a ModelRequest, inserts placeholder UserPromptPart
  • Warnings: Emits UserWarning when repairs are made (configurable via warn=True/False)

Usage

from pydantic_ai import Agent
from pydantic_ai.capabilities import RepairToolOrphans

agent = Agent('anthropic:claude-sonnet', capabilities=[RepairToolOrphans()])

Files changed

  • pydantic_ai_slim/pydantic_ai/capabilities/repair_tool_orphans.py — capability implementation (250 lines)
  • pydantic_ai_slim/pydantic_ai/capabilities/__init__.py — register in exports and CAPABILITY_TYPES
  • tests/test_repair_tool_orphans.py — 16 tests (unit + integration)
  • tests/test_capabilities.py — snapshot update for registry

Test plan

  • 15 unit tests covering: empty/clean history, dangling regular/builtin calls, trailing responses, orphaned returns/retries, empty request placeholders, partial answers, metadata preservation, warning behavior, multi-turn poisoned conversations
  • 1 integration test with Agent + FunctionModel using poisoned message history
  • All 424 existing capability tests pass (2 pre-existing failures unrelated to this change)

Closes #4728

🤖 Generated with Claude Code