Up to now we built an agent that processes user input and returns structured, predictable responses. The agent uses dependencies for contextual information and can access and modify project data via Function Tools.
We now extend the agent so it can use past interactions: it will access and reuse prior messages through a shared conversation history. In Part 3 we already examined the agent’s internal communication using result.all_messages() . Looking at this concept was actually the first step toward understanding how the conversation memory in Pydantic AI works.
Introducing Pydantic AI Message History
Every time our agent runs, it exchanges a sequence of structured messages: system prompts, user queries, model responses and any function/tool calls. Until now each run_sync() call was isolated and stateless. Pydantic AI Agent in Python allows us to pass a message_history argument to run_sync() so that we can reuse previous messages across runs. Using a message_historyis important because it makes the agent’s behavior consistent across multiple interactions and moves the agent from isolated single-turn responses toward reproducible, context-aware multi-turn workflows.
Extending Our Agent with a Session
To make this process easier to manage, we’ll introduce a simple session class that wraps around our existing TaskAgent. This AgentSession will handle the message history automatically by storing every new message after each run into a list and reusing them for the next one. It does so by passing the list of past results to the agent on every run via the message_history parameter.
session.py
class AgentSession:
def __init__(self, agent):
self.agent = agent
self.history = []
def run(self, query: str, deps) -> str:
result = self.agent.run_sync(query, deps=deps, message_history=self.history)
self.history.extend(result.new_messages())
return result.output
This way, the agent uses previous user inputs and model outputs to answer subsequent queries. With this step, we move from single-run executions to a persistent session model that maintains state across interactions.
Putting the Session into Action
Let’s see this in practice by updating our app to use our existing TaskAgent together with the new AgentSession.
app.py
from dotenv import load_dotenv
from LearnPydanticAI.agent import TaskAgent
from LearnPydanticAI.session import AgentSession
from LearnPydanticAI.dependecies import TaskDependency
load_dotenv() # take environment variables from .env.
agent = TaskAgent()
session = AgentSession(agent.agent)
deps = TaskDependency(username="sudo", project="LearnPydanticAI")
answer1 = session.run(query="I want to learn more about Pydantic AI.", deps=deps)
print(answer1)
answer2 = session.run(query="Update its creator to joe", deps=deps)
print(answer2)
In this example:
- We initialize the
TaskAgentand wrap it with anAgentSession. - The first query stays the same and asks the agent to create a task.
- The second query refers to “its user”. Because the session preserves the message history, the agent can resolve this to the previously created task and apply the change.
This demonstrates the easiest way our agent can maintain continuity across multiple runs.
% poetry run python src/LearnPydanticAI/app.py
task='Learn about Pydantic AI'
description='1. Research about Pydantic AI.\n2. Learn about its features and capabilities.\n3. Experiment with simple examples.'
priority=3
project='LearnPydanticAI'
created_by='sudo'
number_of_open_tasks=3
success=True
task='Learn about Pydantic AI'
description='1. Research about Pydantic AI.\n2. Learn about its features and capabilities.\n3. Experiment with simple examples.'
priority=3
project='LearnPydanticAI'
created_by='joe'
number_of_open_tasks=3
success=True
Now that we’ve seen the session in action, let’s take a look under the hood to understand how PydanticAI manages message history internally.
How It Works Behind the Scenes
When run_sync() is called with a message_history, Pydantic AI automatically:
- Combines the historical messages with the new user input,
- Generates a complete conversational context for the model,
- Returns all messages including historical and newly created messages.
By using the message_history we effectively allow the agent to “remember” everything that has happened so far in the session.
Further Pydantic AI Message History Mechanisms
For the purpose of this tutorial, we are not going to extend our agent more than with a session to automatically handle message history. However, Pydantic AI offers additional capabilities for working with message history that go beyond a simple session. We’ll explore in the following sections more mechanisms that Pydantic AI offers to persist history, process it before each run and dynamically adapt it based on the agents context.
Persisting and Reloading Message History
In many cases, we might want to preserve the conversation history beyond a single runtime — for example, to store it on disk, log it for later inspection, or share it between services.
Pydantic AI provides a convenient way to serialize and reload the entire message history using the ModelMessagesTypeAdapter utility. This ensures that all messages, including user queries, system prompts, and function calls, can be stored as structured data and later validated when reloaded.
result1 = self.agent.run_sync("Create a new task", deps=deps, message_history=self.history)
history_step_1 = result1.all_messages()
# Convert history into JSON-serializable Python objects
python_objects = to_jsonable_python(history_step_1)
# Validate and reload the same history structure
history_from_step_1 = ModelMessagesTypeAdapter.validate_python(python_objects)
# Reuse the restored history in a new run
result2 = self.agent.run_sync("Change its user to joe", deps=deps, message_history=history_from_step_1)
This pattern is ideal when you want your agent’s state to persist between sessions or when running long-lived applications where context continuity matters. The agent now can pause, resume, or transfer its memory safely.
Processing and Filtering History
Sometimes, it’s neither necessary nor desirable to forward the entire message history to the model. Pydantic AI provides history_processors, which allow you to inspect and modify the history before each run.
Common use cases include:
- Privacy filtering: removing user-identifying information before sending context to the model.
- Token optimization: truncating older messages to save cost and improve performance.
- Custom logic: transforming messages, rephrasing context, or aggregating prior results.
async def keep_recent_messages(messages: list[ModelMessage]) -> list[ModelMessage]:
"""Keep only the last 5 messages to manage token usage."""
return messages[-5:] if len(messages) > 5 else messages
def _init_agent(self) -> Agent:
agent = Agent(
model = MistralModel(model_name=os.getenv("LLM_MISTRAL_MODEL"), provider=MistralProvider(api_key=os.getenv("MISTRAL_API_KEY"))),
output_type=TaskModel,
deps_type=TaskDependency,
history_processors=[keep_recent_messages]
)
Context-Aware History Processing
For even more control, Pydantic AI supports the context_aware_processor, which lets you modify message history based on the current run context.
This is useful when certain dependencies or runtime parameters should influence how much of the history is visible to the model. For instance, you might:
- Hide system prompts when the user has limited access rights,
- Provide additional context only for specific projects or tasks, or
- Apply specialized summarization logic depending on the user’s role.
async def keep_recent_messages(ctx: RunContext[TaskDependency], messages: list[ModelMessage]) -> list[ModelMessage]:
"""Keep only the last 5 messages for non sudo users."""
if ctx.deps.username != "sudo":
return messages[-5:] if len(messages) > 5 else messages
return messages
def _init_agent(self) -> Agent:
agent = Agent(
model = MistralModel(model_name=os.getenv("LLM_MISTRAL_MODEL"), provider=MistralProvider(api_key=os.getenv("MISTRAL_API_KEY"))),
output_type=TaskModel,
deps_type=TaskDependency,
history_processors=[keep_recent_messages]
)
With context_aware_processor, the history can dynamically adapt to the agent’s current execution context, giving you fine-grained control over what information the model sees at each step.
Relevance for Real-World Applications
Message history plays an essential role when integrating AI agents into real-world systems. It enables agents to maintain context across interactions and to behave more consistently over time. By storing and reusing messages, developers can create agents that understand past actions, follow long-term goals, and build on previous results.
In practical scenarios, conversation memory allows agents to:
- Maintain context over multiple user sessions
- Improve accuracy by referencing earlier information
- Adapt behavior based on previous interactions
- Provide personalized experiences for different users
- Reduce redundancy by avoiding repeated inputs
Using message history in PydanticAI combines the flexibility of natural language interfaces with the reliability of structured, contextual memory. This makes it a foundational concept for building stateful, context-aware AI agents that can operate effectively in complex, real-world environments.
Continuing the Series
With this part, we have extended our agent to support persistent message history, a key feature for building context-aware and stateful AI agents in Python. In the next part, we will shift our focus to multi-agent systems in Pydantic AI and explore how multiple agents can collaborate, delegate tasks, and work together to solve more complex problems in a structured and predictable way.
💻 Code on GitHub: hamluk/LearnPydanticAI/part-4
📘 Read Part 5: Advanced Pydantic AI Agents: Multi-Agent System
Top comments (0)