Pydantic AI just shipped the biggest API change since launch. Capabilities, hooks, and agent specs landed in v1.71+, and they fundamentally change how you extend agents.
We maintain 5 open-source libraries built on top of Pydantic AI: pydantic-ai-shields (formerly pydantic-ai-middleware), pydantic-ai-subagents, pydantic-ai-summarization, pydantic-ai-backend, and the full-stack AI agent template. All five have been migrated. This article covers what changed, why it matters, and real before/after code from our repos.
What Are Capabilities?
Capabilities are reusable, composable units of agent behavior. Instead of threading multiple configuration arguments separately -- tools here, instructions there, model settings somewhere else -- a capability bundles everything into a single capabilities parameter on the Agent constructor.
Each capability can provide:
-
Tools (via
get_toolset()) - Instructions (static strings or dynamic callables)
- Model settings (per-step configuration)
- Lifecycle hooks (before/after/wrap patterns for runs, model requests, tool calls)
- Tool preparation (filter or modify tool definitions per step)
The base class is AbstractCapability. You subclass it, override the methods you need, and pass instances to the agent:
from pydantic_ai import Agent
from pydantic_ai.capabilities import AbstractCapability
agent = Agent(
"openai:gpt-4.1",
capabilities=[MyCapability(), AnotherCapability()],
)
Multiple capabilities compose automatically. Before-hooks fire in order (cap1 then cap2), after-hooks fire reversed (cap2 then cap1), and wrap-hooks nest as middleware layers. This is not something we had to build -- the framework handles it.
What Are Hooks?
Hooks are the lifecycle interception points within capabilities. Pydantic AI provides hooks at four levels:
-
Run hooks --
before_run,wrap_run,after_run,on_run_error -
Node hooks --
before_node_run,wrap_node_run,after_node_run,on_node_run_error -
Model request hooks --
before_model_request,wrap_model_request,after_model_request,on_model_request_error - Tool hooks -- split into validation and execution phases, each with before/wrap/after/error variants
Plus prepare_tools for filtering tool visibility per step.
That's roughly 20 hook points across 4 lifecycle levels. Error hooks use a neat pattern: raise to propagate, return to recover. If your error handler raises the original exception, it propagates unchanged. Raise a different exception to transform the error. Return a result to suppress it entirely.
For simple use cases, the Hooks capability gives you decorator-based registration without subclassing:
from pydantic_ai.capabilities import Hooks
hooks = Hooks()
@hooks.on.before_model_request
async def log_request(ctx, request_context):
print(f"Sending {len(request_context.messages)} messages")
return request_context
agent = Agent("openai:gpt-4.1", capabilities=[hooks])
What Are Agent Specs?
Agent specs separate agent configuration from code entirely. You define your agent in YAML or JSON:
model: anthropic:claude-opus-4-6
instructions: You are a helpful assistant.
capabilities:
- WebSearch
- Thinking:
effort: high
Then load it:
agent = Agent.from_file("agent.yaml")
Capabilities that implement get_serialization_name() and from_spec() are automatically available. This means your custom capabilities can be YAML-driven too.
How Our Libraries Migrated
pydantic-ai-middleware to pydantic-ai-shields
This was the most dramatic change. Our middleware library had grown to include MiddlewareAgent, MiddlewareChain, ParallelMiddleware, ConditionalMiddleware, PipelineSpec, config loaders, a compiler -- a whole parallel abstraction layer.
We deleted all of it.
The v0.3.0 release renamed the package to pydantic-ai-shields and rebuilt everything as capabilities.
Before (middleware era):
from pydantic_ai_middleware import MiddlewareAgent, CostTrackingMiddleware
middleware_agent = MiddlewareAgent(
agent,
middlewares=[CostTrackingMiddleware(budget_limit_usd=5.0)]
)
result = await middleware_agent.run("Hello")
After (capabilities era):
from pydantic_ai import Agent
from pydantic_ai_shields import CostTracking, PromptInjection, PiiDetector
agent = Agent(
"openai:gpt-4.1",
capabilities=[
CostTracking(budget_usd=5.0),
PromptInjection(sensitivity="high"),
PiiDetector(detect=["email", "ssn", "credit_card"]),
],
)
result = await agent.run("Hello")
No wrapper agent. No middleware chain. The shields are just capabilities that hook into before_run, after_run, prepare_tools, and before_tool_execute as needed.
The new package ships 10 capabilities: CostTracking, ToolGuard, InputGuard, OutputGuard, AsyncGuardrail for infrastructure, plus PromptInjection, PiiDetector, SecretRedaction, BlockedKeywords, and NoRefusals as zero-dependency content shields.
pydantic-ai-subagents
The subagents library now exposes SubAgentCapability that bundles the subagent toolset and dynamic instructions:
from pydantic_ai import Agent
from subagents_pydantic_ai import SubAgentCapability, SubAgentConfig
agent = Agent(
"openai:gpt-4.1",
capabilities=[SubAgentCapability(
subagents=[
SubAgentConfig(
name="researcher",
description="Researches topics",
instructions="You are a research assistant.",
),
],
)],
)
The capability provides tools via get_toolset() and injects instructions via get_instructions(). It also supports agent spec serialization.
pydantic-ai-summarization
Four capabilities replace the old middleware-based context management:
-
SummarizationCapability-- triggers LLM summarization when thresholds are reached -
SlidingWindowCapability-- zero-cost alternative that discards oldest messages -
LimitWarnerCapability-- injects warnings when limits approach -
ContextManagerCapability-- full package: token tracking, auto-compression, tool output truncation
from pydantic_ai import Agent
from pydantic_ai_summarization.capability import ContextManagerCapability
agent = Agent(
"openai:gpt-4.1",
capabilities=[ContextManagerCapability(max_tokens=100_000)],
)
pydantic-ai-backend
Our filesystem toolkit became ConsoleCapability -- bundling tools, instructions, and permission enforcement:
from pydantic_ai import Agent
from pydantic_ai_backends import ConsoleCapability
from pydantic_ai_backends.permissions import READONLY_RULESET
agent = Agent(
"openai:gpt-4.1",
capabilities=[ConsoleCapability(permissions=READONLY_RULESET)],
)
Key Takeaways
1. Delete your abstraction layers. We removed thousands of lines of middleware code. The framework does it better now.
2. Composition is free. Multiple capabilities stack without you writing any merge logic.
3. Agent specs change deployment. Define agent behavior in YAML, deploy by changing a config file.
4. The migration is mechanical. For each middleware hook, there's a direct capability equivalent.
5. Think in capabilities, not agents. The old pattern: build a specialized agent. The new pattern: build a capability, attach it to any agent.
Top comments (0)