Skip to content

Latest commit

 

History

History
178 lines (158 loc) · 14.8 KB

compatibility.md

File metadata and controls

178 lines (158 loc) · 14.8 KB

Pydantic AI compatibility

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.

Evidence and reproduction

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 check

The --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.

Integration and test coverage

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.

Observed boundaries and remaining coupling

  • 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_JSON marker 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_session also 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 as failed). Calls without a matching stored action are resolved as unknown; this does not prove the external action failed or prevent its late execution. StepPersistence and pydantic_ai.durable_exec were compared and deferred in persistence.
  • A history processor's output becomes the run's history: core assigns ctx.state.message_history[:] = processed and recomputes the new-message index from run_id. The runtime therefore never transforms messages that carry the current run's id, so new_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.0 and core 2.38.0: TieredCompaction, ClearToolResults and SummarizingCompaction install and run with a FunctionModel. SummarizingCompaction rewrites the run's all_messages() to the summary plus retained tail, spends one request of the run's UsageLimits (the runtime's max_turns) on the summary, needs a resolved Model for the subscription transport, writes a plain-text summary rather than the runtime's structured CompactionResult, and the package warns about renamed classes. These history and budget differences are why the runtime retains its own policy. Applications can attach Harness through AssistantDefinition.capabilities after 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 and RunContext.enqueue to deliver mid-tool steering. Screen retention sanitizes public RunContext.messages after 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_messages still arranges completed and pending calls around upstream deferred-resumption behavior. Keep the real mixed backend/host continuation case green when changing it.

GPT-Live client delegation

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.