cd /news/developer-tools/pydantic-ai-keeps-one-growing-messag… · home topics developer-tools article
[ARTICLE · art-107063] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Pydantic AI keeps one growing message list per run — and re-sends the whole thing every step

A developer's analysis of Pydantic AI's run loop reveals that the framework maintains a single mutable conversation list per run and re-sends the entire accumulated history—including tool outputs—on every model step, leading to O(n²) cumulative token usage. The developer recommends measuring and pricing real runs, and introduces open-source tools tokenscope and ci-guardrail to quantify and gate token costs in CI.

read3 min views1 publishedAug 22, 2026

Pydantic AI gives you a clean, typed agent: define an Agent

, hand it tools, call agent.run(...)

, and it loops — model call, tool call, model call — until it produces a validated result. The typed ergonomics are great. What the quickstart doesn't spell out is what the model receives on each pass of that loop.

I read the run graph (pydantic_ai_slim/pydantic_ai/_agent_graph.py

on main

) to find out. The mechanism is structural, and it's the same shape I found in the OpenAI Agents SDK and smolagents.

Each run holds a single mutable conversation list on its state:

message_history: list[_messages.ModelMessage] = dataclasses.field(default_factory=list[_messages.ModelMessage])

On every model step the graph appends to it — first the outgoing request, then the model's response:

ctx.state.message_history.append(self.request)
...
ctx.state.message_history.append(response)

Nothing is removed. The list only grows: request, response, request, response — with tool calls and, crucially, tool outputs riding inside those messages.

When the graph builds the input for the next model call, it takes the entire accumulated history — a full copy:

messages = ctx.state.message_history[:]
...
messages[:] = _clean_message_history(ctx.state.message_history)

That [:]

is the whole conversation to date. So on step 1 the model sees your prompt; on step 2 it sees your prompt + step 1's request + step 1's response (including the tool output); on step 5 it sees all of that plus steps 2–4. The payload you pay for grows every single step, and the heaviest passengers are usually the tool outputs — the search results, file contents, and API responses you least want re-uploaded five times.

A run of n steps sends roughly 1 + 2 + 3 + … + n

copies of history — O(n²) cumulative tokens in the step count. A 3-step agent is fine. A 12-step agent that reads a couple of files is not: each file's contents rides along on every later step. The run still succeeds, your tests still pass — the only artifact is a bigger number on the usage line, and you don't see it until the invoice.

Pydantic AI hands you the full transcript back (result.all_messages()

) and every run method takes a message_history

parameter:

message_history: Sequence[_messages.ModelMessage] | None = None

"History of the conversation so far." That's the lever: across a multi-turn conversation you decide what prior history to replay, so you can pass a trimmed or summarized history into the next run instead of the raw accumulation. Inside a single deep tool-loop the re-send is inherent to how tool-calling works (it's true of every framework) — which is exactly why the move is to measure it, not assume it's free.

Before refactoring anything, put a number on it — the token cost of your run, priced in dollars, not tokens. That's what @wartzar-bee/tokenscope does (

npm i @wartzar-bee/tokenscope

): it takes real usage and prices each bucket — input, output, cache-write (~1.25×), cache-read (~0.1×) — into an actual per-run dollar figure, so "the 12-step version costs 4× the 4-step version" stops being a hunch.And if this runs in CI, gate it: wartzar-bee/ci-guardrail is an Apache-2.0 GitHub Action (built on tokenscope) that fails the check when a run crosses an absolute

max-usd

ceiling — so the quadratic step doesn't reach production as a silent 4× before anyone notices.

- uses: wartzar-bee/ci-guardrail@v1
  with:
    max-usd: "0.50"

If you run Pydantic AI: how many steps does your deepest agent take, and how big are the tool outputs it re-sends on every one? Worth pricing one real run before the next invoice does it for you.

── more in #developer-tools 4 stories · sorted by recency
── more on @pydantic ai 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/pydantic-ai-keeps-on…] indexed:0 read:3min 2026-08-22 ·