Handle pydantic-ai's new `RetryFeedbackPart`

Draft
DS
1mo ago
+106-2

This pull request was posted by Claude Code using claude-opus-5 on behalf of David.

David has not reviewed the code; the two decisions below are AI-made and are the thing worth reviewing.

Companion to pydantic/pydantic-ai#7353, which splits RetryPromptPart by what the retry answers: one against a tool call becomes ToolReturnPart(outcome='retried'), one that answers nothing becomes a new RetryFeedbackPart. The new part lands in two assert_never chains here — the signal they were written to give.

The two chains got different answers

They ask different questions, so they were decided separately rather than one pattern-matched onto the other.

compaction/_shared.py — count it. The question is what text occupies the model's context window. RetryFeedbackPart is stored model-neutral and rendered at request time as a mid-conversation system message (or <system>-tagged user text where the provider takes none), so it costs the window exactly like the SystemPromptPart branch above it. Not counting it would under-estimate and compact late.

It counts model_response() rather than str(part.content): a validation failure carries a list of ErrorDetails, and the model is shown them rendered. The per-cause sentence framing it as the harness speaking is chosen during rendering, so it isn't counted — a small fixed undercount, in an estimator that already counts no tool definitions at all.

conversation_search/_toolset.py — index it. The question is whether a part reads as conversation worth recalling. The neighbouring RetryPromptPart branch already answers yes for the tool-bound half ("worth recalling, so index it in full"). Why a run changed course is the same thing worth finding whether or not the retry named a tool, so indexing only one half would make recall depend on which part type pydantic-ai reached for — an artifact of the split, not a property of the text. And unlike ToolAvailabilityDeltaPart directly below, this is prose the model read and acted on, not tool-list bookkeeping.

Rendered as Retry: {model_response()}, with no tool name, since the part is never bound to a call. The display excerpt truncates at 500 like every other content-bearing branch while the index keeps the full text — a validation_error renders one fenced JSON block per error, so it is the branch most likely to run long.

CI is red, and cannot be green yet

RetryFeedbackPart is imported at module scope in two runtime modules, and it exists only on pydantic-ai#7353. So this is not a typecheck-only artifact — lint, test (resolves the lock at pydantic-ai-slim==2.28.0), test-floor (--resolution lowest-direct, same 2.28.0), test-latest (latest published, still no such class) and coverage all fail on ImportError. Please don't re-triage it as a lint quirk.

This is the #586 shape — handling plus a floor bump to the release that introduced the part — except the release doesn't exist yet, so uv.lock can't name it.

Owed once pydantic-ai ships the part:

  1. Bump the pydantic-ai-slim floor to that release and relock.
  2. Fix the stale comment above the floor in pyproject.toml: it still justifies the pin entirely in terms of ToolAvailabilityDeltaPart landing in 2.23.0, while the value has read >=2.28.0 since #586 bumped it without touching the comment.
  3. Consider hoisting test_compaction.py's function-local import to module scope, which is what #579's review did once its floor guaranteed the class.

Verified locally against the actual target instead, with pydantic-ai#7353 installed editable: the type checker reports no errors, tests/compaction + tests/conversation_search pass (645 tests), and both changed modules report 100% coverage with no partial branches.

Ordering

pydantic-ai#7353's harness compat check runs this repo's main, so it stays red until this merges; this PR's own jobs stay red until pydantic-ai releases the part. One of the two has to land red — on the #586 precedent that is pydantic-ai first, then the floor bump here.