Type-Safe Tool Definitions with Pydantic AI
Define agent tools with typed parameters, validated args, clear docstrings, and dependency injection so the model cannot invent shapes your code cannot run.
Search across all documentation pages
Define agent tools with typed parameters, validated args, clear docstrings, and dependency injection so the model cannot invent shapes your code cannot run.
Register Python functions with @agent.tool / @agent.tool_plain (or Tool(...)) so Pydantic AI builds JSON schemas from type hints, validates model-supplied args, and returns results into the agent loop.
@agent.tool_plain when the tool needs no deps; use @agent.tool with RunContext[Deps] when it does.TestModel / eval runs for call patterns.from dataclasses import dataclass
from datetime import date
from pydantic import BaseModel
from pydantic_ai import Agent, RunContext
@dataclass
class BookingDeps:
user_id: str
# db: DatabaseClient # inject real clients in production
class BookingResult(BaseModel):
confirmation_id: str
nights: int
total_usd: float
class HotelAgentOut(BaseModel):
message: str
booking: BookingResult | None = None
agent = Agent(
"openai:gpt-4o-mini", # verify at build
deps_type=BookingDeps,
output_type=HotelAgentOut,
instructions=(
"Help users check availability and book stays. "
"Use tools for facts; finish with HotelAgentOut."
),
)
@agent.tool_plain
def list_room_types() -> list[str]:
"""Return the room types this property offers."""
return ["standard", "deluxe", "suite"]
@agent.tool
def quote_stay(
ctx: RunContext[BookingDeps],
room_type: str,
check_in: date,
nights: int,
) -> dict:
"""Price a stay for the current user.
Args:
room_type: One of standard, deluxe, suite.
check_in: Arrival date (YYYY-MM-DD).
nights: Number of nights between 1 and 30.
"""
if nights < 1 or nights > 30:
return {"error": "nights must be between 1 and 30", "user_id": ctx.deps.user_id}
rates = {"standard": 120.0, "deluxe": 180.0, "suite": 280.0}
if room_type not in rates:
# Returning an error string is ok; raising ModelRetry can re-ask
return {"error": f"unknown room_type {room_type}", "user_id": ctx.deps.user_id}
total = rates[room_type] * nights
return {
"user_id": ctx.deps.user_id,
"room_type": room_type,
"check_in": check_in.isoformat(),
"nights": nights,
"total_usd": total,
}
@agent.tool
def confirm_booking(
ctx: RunContext[BookingDeps],
room_type: str,
check_in: date,
nights: int,
) -> BookingResult:
"""Finalize a booking after the user agrees to the quote.
Args:
room_type: Selected room type.
check_in: Arrival date.
nights: Number of nights.
"""
# Call your booking service with ctx.deps.user_id here.
quote = quote_stay(ctx, room_type, check_in, nights)
if "error" in quote:
raise ValueError(quote["error"])
return BookingResult(
confirmation_id=f"BKG-{ctx.deps.user_id[:4].upper()}-001",
nights=nights,
total_usd=float(quote["total_usd"]),
)
result = agent.run_sync(
"Quote a deluxe room for 2 nights starting 2026-08-01, then book it if under $400.",
deps=BookingDeps(user_id="user-42"),
)
print(result.output)Notes:
| Style | When |
|---|---|
@agent.tool | Needs RunContext / deps |
@agent.tool_plain | No context required |
Agent(..., tools=[fn, Tool(fn, ...)]) | Reuse tools across agents, extra control |
RunContext are model-visible.ToolReturn for model-visible content plus metadata (for example UI data chunks).RunContext[Deps] usage.output_type remains a separate contract for the end of the run.Tools and final output are both structured surfaces.
Do not conflate them: tools intermediate, output_type terminates (in default configurations).
requires_approval / deferred tools - verify at build).ModelRetry with repair instructions.RunContext typing. Wrong deps_type vs annotation is a static bug you want caught early.| Approach | Pros | Cons |
|---|---|---|
| Pydantic AI function tools | Typed, validated, deps-aware | Python-centric |
| MCP tools | Shared tool servers across hosts | Network and auth complexity |
| Provider raw function calling | Minimal framework | Manual schema and loop |
| Instructor only | Great for final extract | No first-class agent tool loop |
| HTTP tools without schema | Fast hack | High arg error rate |
Use models when the model must read structured fields reliably or when multiple tools share a result shape. Primitive returns are fine for simple lookups.
Tools send results back to the model and continue the loop. Output functions / output_type targets end the run with a final value.
Yes via toolsets and prepare hooks that filter tool definitions per step. Use this for stage-gated workflows.
Put it on deps and read ctx.deps inside @agent.tool. Avoid global singletons that break tests.
Define async def tools when you await IO. Pydantic AI supports async agent runs and async tools.
Strict enums reduce junk args. If the catalog changes often, fetch allowed values in a read tool and validate in code.
Yes, but treat that as an explicit handoff with budgets. Nested agents amplify cost and failure modes.
Stream tool events through VercelAIAdapter and render tool cards on the client. Keep execution authority on the server.
Docstring Args are the common path for tool schema descriptions. Use Field constraints on nested models; keep signatures readable.
Typed args, least privilege, timeouts, structured errors, retry caps, logging without secrets, and eval coverage for the top call paths.
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.
Reviewed by Chris St. John·Last updated Jul 16, 2026