Skip to main content

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

PydanticAICubePi
Core abstractionTyped Agent[OutputType] with dependency injectionStateful Agent with a while-loop core — composable via middleware
Structured outputAgent-level — Agent[Deps, OutputType] types every run; NativeOutput / PromptedOutput / ToolOutput modesCall-level — BoundModel.generate_structured(Pydantic, …) returns a validated instance; tool-output mode under the hood (same as pydantic-ai default)
Dependency injectionRunContext[Deps] — deps injected at run timePass context through tool closures or middleware; no DI container
CheckpointingNo built-in persistence layerAppend-only — O(1) DB I/O; backends: memory, SQLite, Postgres, MySQL
Streamingasync for chunk in agent.run_stream()async for event in stream — 11 typed event types including tool lifecycle
Multi-providerAnthropic, OpenAI, Gemini, Groq, and moreAnthropic & OpenAI built in; Provider protocol for custom backends
Provider fallbackNot built inFallbackBoundModel — auto-failover on rate-limit or outage
ObservabilityLogfire (Pydantic's own platform)Native OpenTelemetry — GenAI semconv, OTLP / JSONL; vendor-neutral
MiddlewareNo middleware system8 typed hooks with declarative composition rules
TestingTestModel for deterministic testsFauxProvider — realistic streaming deltas, no API keys needed
Core depspydantic-ai-slim + provider adapterspydantic, anthropic, openai — everything else is an optional extra

A tool-using agent

# PydanticAI
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)
# CubePi
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.