The runtime supports Pydantic AI 2.x starting at 2.38.0 (>=2.38.0,<3).
The release/development baseline is 2.38.0 in uv.lock. CI also tests
2.40.0. The upper
bound prevents an unreviewed major upgrade; it does not assert that every
future 2.x release has been tested. Prefer the lockfile for deployments.
The execution, history and native composition cases in tests/compatibility/
run on both 2.38.0 and 2.40.0 in CI. They use real Agent
execution against FunctionModel; provider requests are disabled. Session
reload replaces the database persistence boundary with a fake, so this is
not a Postgres restart test or a live provider test.
uv sync --locked --extra dev
uv run pytest tests/compatibility -q
uv run --no-sync --with pydantic-ai-slim==2.40.0 --with pydantic-graph==2.40.0 \
python -m pytest tests/compatibility -q
make checkThe --with command creates an isolated overlay; it does not upgrade the
project lock or installed environment. CI runs the compatibility cases on
both versions and keeps the existing full gate on Python 3.12 and 3.13.
Update the comparison target deliberately when reviewing a dependency
upgrade, then review the lockfile diff and run the full gate.
| Component | Tested versions / requirements | Evidence boundary |
|---|---|---|
| Core execution | pydantic-ai-slim and pydantic-graph 2.38.0, 2.40.0 |
Real-agent compatibility cases |
| Vercel UI adapter | pydantic-ai-slim[ui]; starlette>=0.46.2 |
VercelAIAdapter imports on both core versions |
| AG-UI adapter | ag-ui-protocol>=0.1.10 (the ag-ui extra; 0.1.22 locked for development) |
tests/compatibility/test_agui.py: RunAgentInput parsing (AGUIAdapter.build_run_input) and event encoding (AGUIEventStream.transform_stream) over the runtime's turn pipeline; frontend tool round trip, failed results, duplicates, 422/501 |
| Harness | Separate pydantic-ai-harness==0.29.0, requiring core >=2.38.0 |
SummarizingCompaction, Memory, StepPersistence imports on both; no behavior-equivalence claim |
| Durable backends | Separate extras; e.g. temporalio>=1.24.0, dbos>=2.10.0, prefect>=3.7.5 |
Dependency metadata only; no backend installed or exercised |
Adapters and Harness are not runtime dependencies. Their CI import check uses a separate overlay. Successful imports establish dependency compatibility, not integration with sessions, authorization, or streaming. Requirements were inspected in the published core and Harness metadata.
Test names below refer to tests/compatibility/. The runtime uses public
event streaming, steering enqueue and cancellation snapshots. It owns session
state, persistence and finalization around those native operations.
| Area | Decision and public API | Executable evidence / remaining application behavior |
|---|---|---|
| Streaming graph traversal | Composed Agent.run_stream_events() |
test_runtime_stream_order_and_complete_tool_arguments, test_public_stream_has_complete_tools_and_one_trailing_result, and native capability guard/wrapper/event-processor cases; retain event naming, segment IDs, full arguments, one final/completed envelope, and suppression of repeated final text |
| Steering | Composed RunContext.enqueue(priority="asap") and EnqueuedMessagesEvent with the session queue |
Same-run delivery remains once; test_interrupted_steering_remains_pending_for_the_next_turn verifies retry after cancellation before the model call or during a partial response. Acknowledge only after the consuming model request succeeds |
| Cancellation | Composed CancellationToken, RunCancelled.new_messages() / .usage, and RunCancelled.from_cancellation() |
Native cases cover partial responses and tool-task teardown; the runtime saves snapshots through the same assistant-row persistence path, including accepted host results, and completes one cancellation lifecycle |
| Deferred host actions | Keep composing ExternalToolset, DeferredToolRequests, DeferredToolResults |
Host continuations keep the same assistant message and at most one active pending call. Each non-final result is recorded and the next pending call is handed over without a model run; the model resumes only after the final result |
| Tool arguments | Compose ToolCallPart.args_as_dict() |
Five serialization cases plus a real streamed host continuation cover dicts, JSON objects, malformed/non-object JSON and empty args |
| Model-input history cleanup | Replaced by the public Agent run pipeline; the runtime no longer repairs dangling calls or orphaned results itself (#87) |
test_public_history_closes_dangling_calls_including_malformed_args, test_public_history_drops_orphaned_results_before_model_request, test_public_continuation_closes_older_dangling_call_with_deferred_result; the source conversation and pending frontier semantics are preserved |
| Application history policy | Composed as a ProcessHistory capability (HistoryService.processor()), attached to every run of a turn |
test_runtime_clears_old_tool_results_in_model_input_only, test_runtime_summarizes_once_per_turn_through_native_execution, test_runtime_summary_failure_falls_back_without_failing_the_turn, test_runtime_disabled_compaction_leaves_history_to_host_capabilities; the application owns which branch and records supply history, the budget, clearing and the structured summary |
| Session storage and reload | Keep application tree/steering persistence, with the pending host action on the session row; Composed ModelMessagesTypeAdapter for each row's messages, replayed unchanged |
test_session_reload_restores_tree_and_repairs_unfinished_host_action, test_recovery.py, test_later_turns_start_with_what_the_model_saw; flat upstream message serialization does not encode this tree or the pending host action between runs |
| Harness compaction / memory | Deferred as a core dependency; usable by applications through AssistantDefinition.capabilities with the built-in compaction off (the default) |
Evaluated 2026-09-05 with pydantic-ai-harness==0.29.0 on core 2.38.0 (see below); working memory stays a separate per-turn extraction |
| UI protocols | Composed AGUIAdapter.build_run_input and AGUIEventStream on the runtime's native event tap (TurnControl.native_sink); Vercel AI import smoke only |
test_agui.py; the session mapping (thread → session, trailing tool message → continuation, tools → host actions) is application behavior in app/routes/_agui.py; VercelAIAdapter is not wired |
| Definitions / evolving artifacts | Keep versioned application definitions; compose native instructions/capabilities | No replacement of definition, activation, rollback or mutation policy established; existing artifact tests remain authoritative |
| Host attachments | Composed BinaryContent, ImageUrl, DocumentUrl on the user prompt for reference attachments; screenshots stay behind the look_at_screen tool (#90) |
test_reference_attachments_reach_the_model_as_native_content; request-declared host actions use the same ExternalToolset / DeferredToolRequests path (test_request_declared_action_is_called_and_continued) |
| Usage, models, images and toolsets | Keep composing native UsageLimits, model/provider APIs, BinaryContent, FunctionToolset |
Existing tests under tests/unit/services/ and screenshot tests remain regression coverage; this baseline does not justify replacing the subscription transport or extending accounting |
The public API references are agent execution, run control, deferred tools, and capabilities. Decisions above use the executable cases against the stated versions, since the live documentation can advance beyond the lockfile.
- Streamed providers can keep tool arguments as a JSON string. Native
argument parsing preserves those arguments in pending host payloads. Invalid
JSON retains the upstream
INVALID_JSONmarker instead of silently becoming an empty object. - Core's public request pipeline both removes orphaned results and repairs dangling calls. Inspecting only its dangling-call helper understates that support. The tested malformed-history examples are not a guarantee that every provider will accept arbitrary damaged history.
- A turn owns a fresh native cancellation token across its agent runs. Cancellation retains raw native messages and usage, not reconstructed display deltas; host event processors can transform that display. Partial text and completed tools are saved, unresolved calls receive an interrupted outcome, and accepted host results remain on the existing assistant row. External task cancellation still propagates after cleanup. A timeout uses upstream's attached snapshot while keeping its timeout error classification.
cancel_sessionalso interrupts setup. Iterator closure/cancellation and shutdown cancel and drain the producer; a Socket.IO disconnect leaves it running. Ordinary replacement waits for cleanup, and continuations wait without cancelling. These are process/session guarantees, not durable run checkpoints or proof that cancelled external work had no effects.- Queued steering is acknowledged after a successful model response consumes it. Insertion into the native queue alone does not prove delivery, so interrupted delivery stays pending and is retried on the next turn; the turn's saved messages leave that steering out, so it is not replayed twice.
- Session reload restores message branches, queued steering and the pending
host action stored on the session row (
tests/compatibility/test_recovery.py: a cold cache over the same rows accepts the continuation, rejects duplicates, records host-declared failures natively asfailed). Calls without a matching stored action are resolved asunknown; this does not prove the external action failed or prevent its late execution. StepPersistence andpydantic_ai.durable_execwere compared and deferred in persistence. - A history processor's output becomes the run's history: core assigns
ctx.state.message_history[:] = processedand recomputes the new-message index fromrun_id. The runtime therefore never transforms messages that carry the current run's id, sonew_messages()(what the assistant row stores) stays the raw model output, while earlier turns are cleared or summarised for the model only. A follow-up steering run of the same turn receives the processed history and may summarise once more. - Harness compaction, evaluated offline on 2026-09-05 with
pydantic-ai-harness==0.29.0and core 2.38.0:TieredCompaction,ClearToolResultsandSummarizingCompactioninstall and run with aFunctionModel.SummarizingCompactionrewrites the run'sall_messages()to the summary plus retained tail, spends one request of the run'sUsageLimits(the runtime'smax_turns) on the summary, needs a resolvedModelfor the subscription transport, writes a plain-text summary rather than the runtime's structuredCompactionResult, and the package warns about renamed classes. These history and budget differences are why the runtime retains its own policy. Applications can attach Harness throughAssistantDefinition.capabilitiesafter disabling the built-in policy. - Runtime execution uses
Agent.run_stream_events()without private graph imports or state access. A per-run capability uses public node predicates andRunContext.enqueueto deliver mid-tool steering. Screen retention sanitizes publicRunContext.messagesafter the first model request that consumes the image; this remains application policy. Real cases verify one-request image retention and request context across native tasks. assistant_record_to_flat_messagesstill arranges completed and pending calls around upstream deferred-resumption behavior. Keep the real mixed backend/host continuation case green when changing it.
tests/compatibility/test_voice.py exercises the voice bridge with real
Pydantic AI FunctionModel execution, native backend tools, deferred host
results, saved history and cancellation. It also covers ownership and exclusion
between an active voice reservation and asynchronous session administration.
Only provider HTTP/WebSocket boundaries are faked. Unit tests cover the
published Live protocol shapes, event replay, cumulative usage and finalization.
The tests do not exercise a live OpenAI session, browser audio or provider billing.
The installed OpenAI SDK has no Live client surface, and Pydantic AI's native
Realtime API adapter is a different protocol. The optional [voice] transport
therefore uses documented Live HTTP/WebSocket endpoints behind a private adapter.
Backend execution continues exclusively through StreamingService and native
Pydantic AI events/cancellation. Reevaluate the transport when upstream exposes
Live client delegation; do not substitute a Realtime model based on its name.
Codex subscription transport omits max_output_tokens (the runtime max_tokens
model setting): the subscription endpoint rejects it, unlike the public Responses
API. Numeric thinking budgets still map to reasoning effort. Native usage limits
remain runtime checks, not a provider-side generation cap.
Silent host decisions are exercised with native ToolOutput plus
DeferredToolRequests, including hold/action output ordering, suppressed prose,
receipt-only completion, rejected multiple actions, and native cancellation.
Conversation-only voice uses the existing offline transport tests; no provider
allocation is part of these checks.
Codex Fast mode uses native openai_service_tier serialization. The subscription
adapter observes the OpenAI SDK's already-decoded terminal response events through
its public Responses resource and async stream interface, because Pydantic AI
2.38 does not retain Response.service_tier in its public model response. A
request-scoped observer adds only requested/actual tier metadata to the public
StreamedResponse.provider_details; no custom SSE parsing or agent execution is
introduced. test_codex_fast.py exercises real native execution and HTTP wire
serialization with a fake provider, including concurrent requests, missing or
downgraded actual tier and rejected priority requests. Remove this observation
adapter when upstream preserves the field natively with equivalent evidence.