CubePi vs PydanticAI
PydanticAI and CubePi share the same foundation — Pydantic v2, asyncio-native design, and a belief that agents should be plain Python rather than graph-based state machines. Where they diverge is in the abstractions built on top.
PydanticAI centres on type-safe structured output and dependency injection via `RunContext`. CubePi centres on persistent multi-turn conversations, composable middleware hooks, and vendor-neutral OpenTelemetry tracing. Here is how the two compare.
Side-by-side
| PydanticAI | CubePi | |
|---|---|---|
| Core abstraction | Typed Agent[OutputType] with dependency injection | Stateful Agent with a while-loop core — composable via middleware |
| Structured output | Agent-level — Agent[Deps, OutputType] types every run; NativeOutput / PromptedOutput / ToolOutput modes | Call-level — BoundModel.generate_structured(Pydantic, …) returns a validated instance; tool-output mode under the hood (same as pydantic-ai default) |
| Dependency injection | RunContext[Deps] — deps injected at run time | Pass context through tool closures or middleware; no DI container |
| Checkpointing | No built-in persistence layer | Append-only — O(1) DB I/O; backends: memory, SQLite, Postgres, MySQL |
| Streaming | async for chunk in agent.run_stream() | async for event in stream — 11 typed event types including tool lifecycle |
| Multi-provider | Anthropic, OpenAI, Gemini, Groq, and more | Anthropic & OpenAI built in; Provider protocol for custom backends |
| Provider fallback | Not built in | FallbackBoundModel — auto-failover on rate-limit or outage |
| Observability | Logfire (Pydantic's own platform) | Native OpenTelemetry — GenAI semconv, OTLP / JSONL; vendor-neutral |
| Middleware | No middleware system | 8 typed hooks with declarative composition rules |
| Testing | TestModel for deterministic tests | FauxProvider — realistic streaming deltas, no API keys needed |
| Core deps | pydantic-ai-slim + provider adapters | pydantic, anthropic, openai — everything else is an optional extra |
A tool-using agent
from pydantic_ai import Agent
from pydantic_ai.models.anthropic import AnthropicModel
model = AnthropicModel("claude-sonnet-4-6")
agent = Agent(model, system_prompt="You are a helpful weather assistant.")
@agent.tool_plain
async def get_weather(city: str) -> str:
"""Get current weather for a city."""
return f"72F and sunny in {city}"
result = await agent.run("What's the weather in Tokyo?")
print(result.output)
from cubepi import Agent, tool
from cubepi.providers.anthropic import AnthropicProvider
@tool
async def get_weather(city: str) -> str:
"Get current weather for a city."
return f"72F and sunny in {city}"
provider = AnthropicProvider(provider_id="anthropic", api_key="...")
agent = Agent(
model=provider.model("claude-sonnet-4-6"),
tools=[get_weather],
system_prompt="You are a helpful weather assistant.",
)
await agent.prompt("What's the weather in Tokyo?")
Where structured output sits in the API
Both frameworks ship first-class structured output backed by Pydantic. CubePi 0.10 added `BoundModel.generate_structured(Pydantic, ...)`, which injects a synthetic tool from the model's JSON schema, forces the call via `tool_choice`, and validates the response through `output_type.model_validate()` — the same `ToolOutput` mode PydanticAI uses by default.
Where the two diverge is which abstraction owns the contract. PydanticAI lifts the output type up to the agent itself: `Agent[Deps, Sentiment].run(...)` types the whole run as `Sentiment`, and the framework offers `NativeOutput` (provider `response_format` / JSON-schema endpoints) and `PromptedOutput` as alternative modes alongside `ToolOutput`. CubePi keeps the agent loop as free-form text plus tool calls and exposes structured output as a one-shot `BoundModel` call you reach for when you need it — well-suited for extraction subroutines inside a larger multi-turn agent.
CubePi optimises a different primary axis: multi-turn conversations that survive restarts. Append-only checkpointing keeps a thread's write cost flat regardless of conversation length, which matters when you have thousands of concurrent long-lived sessions.
Dependency injection vs middleware
PydanticAI's `RunContext[Deps]` is a clean pattern for injecting services (databases, HTTP clients) into tool functions. CubePi achieves the same via closures — capture your dependencies when you define the tool function — which requires no new abstraction to learn.
Where CubePi adds structure is in cross-cutting concerns: middleware hooks (`before_tool_call`, `transform_context`, `should_stop_after_turn`, etc.) let you add rate-limiting, safety checks, compaction, or subagent orchestration without touching the core agent loop.
Observability: Logfire vs vendor-neutral OTel
PydanticAI integrates with Logfire, Pydantic's own observability platform. It works well if Logfire fits your stack. CubePi emits standard OpenTelemetry spans with GenAI semantic-convention attributes that land in any OTLP-compatible backend — Jaeger, Grafana Tempo, Honeycomb, Datadog, AWS X-Ray — with no vendor dependency. The `cubepi trace` CLI also lets you inspect traces locally from JSONL files without a backend at all.
When PydanticAI is the better fit
PydanticAI is the stronger choice when you want the agent itself typed by its output (`Agent[Deps, OutputType]`), when you need provider-native JSON-schema endpoints (`NativeOutput`) instead of the tool-output mode CubePi uses, or if you are already on Logfire and want tight integration. Choose CubePi when you need production-grade multi-turn persistence, composable middleware, provider failover, or vendor-neutral observability — and you are happy to reach for `BoundModel.generate_structured(...)` as a one-shot when you do need a validated Pydantic instance.