Support realtime `SpeechPart` and `AbstractModel` run models from pydantic-ai `2.28`
Closes #585.
pydantic-ai 2.28 shipped realtime, which makes two upstream changes the harness now has to handle:
RunContext.modelwidened fromModeltoAbstractModel— a realtime model is anAbstractModelbut not a request-responseModel[Any]. 15 sites passedctx.model(or a value derived from it) into something typed forModel/str.SpeechPartjoined theModelRequestPartandModelResponsePartunions — two exhaustiveisinstancechains 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_familywidened toAbstractModel; a newis_realtime_modelhelper).TieredCompactionno-ops on a realtime run. - Summarization / limit
Agent(...)(SummarizingCompaction,ToolOutputLimits): a realtime run reaches these only with no summarizermodel=configured, so raise a clearUserErrorasking for one rather than handingAgenta model it can't run with. (ToolOutputLimitsnow re-raises thatUserErrorpast 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 errorsuv run ruff format --check+uv run ruff check→ cleanuv run pytest(compaction, conversation_search, subagents, dynamic_workflow, tool_output_limits, step_persistence) → 1135 passed