Skip to content

Pydantic AI v2 migration

agent-utilities (and the fleet that inherits from it) runs on Pydantic AI v2 (pydantic-ai-slim>=2.0.0,<3.0.0, pydantic-graph>=2.0.0,<3.0.0). This page records the v2-specific changes so the architecture docs stay in sync with the code.

Why it was a real migration, not a rename

The framework was already on the v1 capabilities API, and our model factory builds typed Model objects (never provider:model strings), so the headline prefix changes (openai:→Responses, grok:xai:, gemini-module removal) don't affect us. But v2 removed several APIs we used, which required real changes:

Removed in v2 Replacement Where
MCPServerSSE / MCPServerStreamableHTTP / MCPServerStdio / FastMCPToolset unified MCPToolset + transports mcp/toolset_factory.py, agent factory, agent_runner, graph builder/executor, core config
pydantic_ai.mcp.load_mcp_servers load_mcp_toolsets graph/executor.py, core/config.py
pydantic_graph.persistence (package) + Graph.run(persistence=) our own BaseStatePersistence (write-only snapshot stores) core/checkpoint/manager.py
Agent.to_a2a() fasta2a.pydantic_ai.agent_to_a2a server/app.py
pydantic_graph.beta.* promoted to top-level pydantic_graph guarded imports across graph/*, orchestration/engine.py
stream.usage() (method) stream.usage (property) graph/_router_impl.py, graph/executor.py
RunUsage.request_tokens / response_tokens input_tokens / output_tokens graph/state.py, observability/token_tracker.py

Behavior change adopted: end_strategy default earlygraceful (set explicitly on the agent factory). Function tools requested alongside an output/deferred tool now run; side-effecting tools stay safe because the tool_guard ApprovalRequiredToolset turns them into DeferredToolRequests that never auto-run before human approval.

The one MCP construction path

v2 collapses every MCP client onto MCPToolset. agent_utilities/mcp/toolset_factory.py is the single place that turns a connection spec into a toolset, so SSL verify + request timeout (threaded through the transport's httpx_client_factory) live in exactly one place.

flowchart LR
    subgraph callers[callers]
      F[agent/factory.py]
      R[orchestration/agent_runner.py]
      B[graph/builder.py]
      C["core/config.py<br/>coordinated KG"]
    end
    callers --> H{{mcp/toolset_factory.py}}
    H -->|url ending /sse| SSE[SSETransport]
    H -->|http url| HTTP[StreamableHttpTransport]
    H -->|command| STDIO[StdioTransport]
    SSE --> MT[MCPToolset]
    HTTP --> MT
    STDIO --> MT
    MT --> AG[Agent toolsets]

Packaging / extras

  • Base agent / agent-headless extras: pydantic-ai-slim[mcp,openai,anthropic,ag-ui,ui,web,cli] (fastmcpmcp; the removed a2a extra → a direct fasta2a[pydantic-ai]>=0.6.1 dependency; anthropic added as a default-bundle provider). Per-provider opt-in extras (agent-google, agent-groq, agent-mistral, agent-anthropic, agent-huggingface) unchanged in shape.
  • agent-webui narrowed from the full pydantic-ai meta to pydantic-ai-slim[ui] (it uses the v2 Agent.to_web()).

Native ergonomics wired (synergy)

Two v2-native capabilities are wired into the agent factory as opt-in synergy (opt-in because both are expensive / behavior-changing; our richer custom systems — KG memory, ontological guardrails, multi-tier Monty sandbox, multiplexer, held-turns — stay):

  • Thinking(effort=) — native provider extended thinking, added to every built agent when create_agent(thinking_effort='low'|'medium'|'high') or the AGENT_THINKING_EFFORT config setting is set (default off). It runs natively where the provider supports reasoning and no-ops elsewhere, and composes with the per-call sampling profile (which still threads the vLLM enable_thinking knob via extra_body).
  • defer_tool_loading=True (create_agent arg) — wraps agent-local toolsets in v2's DeferredLoadingToolset so they appear as a compact catalog and load on demand, cutting prompt bloat for tool-heavy agents. Orthogonal to the cross-process multiplexer (find_tools/load_tools).

v2 also auto-injects ToolSearch and a pending-message-drain (mid-run steering) capability natively — both visible in every built agent's root_capability tree.

Native protocol adapters vs. our plugins

v2's native UI/protocol adapters live in pydantic_ai.ui: AG-UI (ag_ui) and Vercel AI (vercel_ai), plus Agent.to_web() (browser chat) and Agent.to_cli() (interactive terminal chat). These are not ACP. Zed's Agent Client Protocol is still provided by our external plugin (protocols/acp_adapter.py + acp_providers.py on pydantic-acp + acpkit).

ACP on v2 — reconciled via dependency override

pydantic-acp (0.9.7, the optional [acp] extra) declares pydantic-ai-slim==1.106.0, but this is an over-strict metadata pin, not a real code incompatibility: every pydantic_ai symbol pydantic-acp imports — AgentRunResultEvent, ModelRequestContext, CombinedCapability, DeferredToolResults, ToolApproved/ToolDenied, FunctionModel, OutputSpec, … — exists in 2.0.0, and pydantic_acp (incl. create_acp_agent, AcpSessionContext, acp.schema) imports and runs unchanged on pydantic-ai 2.0.0. (acpkit does not pin pydantic-ai outside its unused dev extra.)

We therefore relax that one transitive pin with a dependency override rather than forking or dropping ACP:

  • pyproject.toml [tool.uv] override-dependencies = ["pydantic-ai-slim>=2.0.0,<3.0.0"] — for uv lock / uv sync.
  • overrides.txt (repo root) — the same override for uv pip install --override overrides.txt / UV_OVERRIDE.
  • docker/Dockerfile builder sets ENV UV_OVERRIDE=/src/overrides.txt (no-op for [serving], which has no acp; future-proofs acp-inclusive images).
  • CI: backend-parity-nightly (which pulls [test-backends][acp]) installs via uv pip install --override overrides.txt (plain pip cannot relax a transitive pin).

With the override, [acp]/[test]/[all] resolve to pydantic-ai-slim 2.0.0 + pydantic-acp 0.9.7 + acpkit 0.9.7 + agent-client-protocol 0.9.0 cleanly. Remove the override once pydantic-acp ships a release whose pin admits v2. Plain pip install .[acp] is unsupported (no override mechanism) — use uv.