Support realtime `SpeechPart` and `AbstractModel` run models from pydantic-ai `2.28`

Merged
DO
3mo ago
+388-89

Closes #585.

pydantic-ai 2.28 shipped realtime, which makes two upstream changes the harness now has to handle:

  1. RunContext.model widened from Model to AbstractModel — a realtime model is an AbstractModel but not a request-response Model[Any]. 15 sites passed ctx.model (or a value derived from it) into something typed for Model/str.
  2. SpeechPart joined the ModelRequestPart and ModelResponsePart unions — two exhaustive isinstance chains no longer covered the union.

Floor bumped to pydantic-ai-slim>=2.28.0 (the release that introduced these symbols — the code imports them, matching the existing ToolAvailabilityDeltaPart floor rationale right above it).

Forward-resilience (the design point, per the version policy)

docs/version-policy.md states that adding new message parts is not a breaking change and consumers must "code defensively." So the part-consuming sites are now exhaustive at type-check, graceful at runtime: a SpeechPart branch is handled explicitly, and the runtime fallback for any future upstream part skips it rather than raising — assert_never stays under TYPE_CHECKING only. This means the next new part surfaces as a typecheck decision, not a runtime crash for anyone who upgrades pydantic-ai ahead of us. conversation_search also stops assuming "the only remaining request part is RetryPromptPart".

Per-site decisions

  • SpeechPart (compaction/_shared.py, conversation_search/_toolset.py): count / index the transcript (the billed words), not the raw audio. Audio-only parts contribute nothing.
  • Token-trigger / family / target sites: a realtime model has no request-response context window, so the token triggers skip it (resolve_token_trigger, resolve_context_window, _target, _model_family widened to AbstractModel; a new is_realtime_model helper). TieredCompaction no-ops on a realtime run.
  • Summarization / limit Agent(...) (SummarizingCompaction, ToolOutputLimits): a realtime run reaches these only with no summarizer model= configured, so raise a clear UserError asking for one rather than handing Agent a model it can't run with. (ToolOutputLimits now re-raises that UserError past its truncate-fallback so the config error surfaces.)
  • Subagent inherited-model (SubAgentToolset, DynamicWorkflowToolset): fall back to the sub-agent's own default when the parent run is realtime.

Tests

Added realtime coverage: SpeechPart transcripts are counted/indexed while audio-only speech isn't; realtime runs skip token triggers and leave tiered/sliding-window untouched; summarizing/limits raise the config UserError. 100% branch coverage on every touched source file.

Verification (against released pydantic-ai 2.28.0)

  • uv run pyright → 0 errors
  • uv run ruff format --check + uv run ruff check → clean
  • uv run pytest (compaction, conversation_search, subagents, dynamic_workflow, tool_output_limits, step_persistence) → 1135 passed