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 early → graceful (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-headlessextras:pydantic-ai-slim[mcp,openai,anthropic,ag-ui,ui,web,cli](fastmcp→mcp; the removeda2aextra → a directfasta2a[pydantic-ai]>=0.6.1dependency;anthropicadded as a default-bundle provider). Per-provider opt-in extras (agent-google,agent-groq,agent-mistral,agent-anthropic,agent-huggingface) unchanged in shape. agent-webuinarrowed from the fullpydantic-aimeta topydantic-ai-slim[ui](it uses the v2Agent.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 whencreate_agent(thinking_effort='low'|'medium'|'high')or theAGENT_THINKING_EFFORTconfig 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 vLLMenable_thinkingknob viaextra_body).defer_tool_loading=True(create_agentarg) — wraps agent-local toolsets in v2'sDeferredLoadingToolsetso 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"]— foruv lock/uv sync.overrides.txt(repo root) — the same override foruv pip install --override overrides.txt/UV_OVERRIDE.docker/Dockerfilebuilder setsENV 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 viauv 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.