Pydantic AI leaned into the one thing Python agent frameworks were missing: type safety at the tool boundary. Define a tool as a function with Pydantic-validated inputs and outputs, and the framework handles the LLM-side schema and the runtime-side coercion. It's the right shape for production agents.

Lumbox as Pydantic AI tools

from pydantic_ai import Agent
from pydantic import BaseModel
from lumbox import Lumbox

lumbox = Lumbox(api_key=os.environ["LUMBOX_API_KEY"])

class Inbox(BaseModel):
    inbox_id: str
    address: str

class OtpResult(BaseModel):
    otp: str
    received_at: str

email_agent = Agent(
    "anthropic:claude-opus-4-7",
    system_prompt="You handle email verification for signup flows.",
)

@email_agent.tool_plain
def create_inbox(purpose: str) -> Inbox:
    """Create a fresh inbox for a verification task."""
    inbox = lumbox.inboxes.create(display_name=purpose)
    return Inbox(inbox_id=inbox.id, address=inbox.address)

@email_agent.tool_plain
def wait_for_otp(inbox_id: str, timeout: int = 120) -> OtpResult:
    """Block until an OTP arrives in the inbox."""
    result = lumbox.inboxes.wait_for_otp(inbox_id, timeout=timeout)
    return OtpResult(otp=result.otp, received_at=result.received_at)

Why type-safe tools matter for email

When an agent calls wait_for_otp with a timeout, you don't want a runtime error because the LLM passed "60s" instead of 60. Pydantic coerces at the tool boundary. Same for inbox IDs — Pydantic ensures you always get a well-formed inbox_id, never None.

Error handling

If the OTP wait times out, the tool raises — Pydantic AI surfaces the typed error back to the agent, which can retry or escalate. Cleaner than parsing string error messages out of generic HTTP responses. lumbox.co.