GitHub
Back to course
Lesson 4
45 min

Sessions

Conversation Memory - Build stateful agents that remember context

The Problem: Stateless by Default

By default, each call to an agent is independent. The agent has no memory of previous messages. This happens because each agent.run() starts fresh. The LLM only sees the current message, not previous ones.

stateless.py
# First call
result1 = agent.run_sync('My name is Pablo')
# "Nice to meet you, Pablo!"

# Second call (new context, no memory)
result2 = agent.run_sync("What's my name?")
# "I don't know your name. Could you tell me?"

The Solution: Message History

To create memory, you pass previous messages to the next call. The LLM sees the entire conversation and can reference previous context.

+------------------------------------------------------------------+ | MESSAGE HISTORY FLOW | | | | Call 1: | | +----------------+ | | | User: "Hi" | ---> LLM ---> "Hello! How can I help?" | | +----------------+ | | messages = [user_1, assistant_1] | | | | Call 2 (with history): | | +----------------+ | | | User: "Hi" | | | | Asst: "Hello!" | ---> LLM ---> "As I mentioned, I'm here | | | User: "What | to help with anything!" | | | did you say?" | | | +----------------+ | +------------------------------------------------------------------+

Using Message History

PydanticAI tracks messages in each run. You can pass them to the next run.

Key methods:

  • result.new_messages(): Messages from this run only
  • result.all_messages(): All messages including history
with_history.py
from pydantic_ai import Agent

agent = Agent(
    'openai:gpt-4o',
    instructions='Be helpful. Remember what the user tells you.'
)

# First message
result1 = agent.run_sync('My name is Pablo and I work in fintech.')
print(f"Agent: {result1.output}")

# Continue with history
result2 = agent.run_sync(
    'What do I do for work?',
    message_history=result1.new_messages()  # Pass previous messages
)
print(f"Agent: {result2.output}")
# Agent: You work in fintech!

Building a Chat Session Class

For real applications, you need a proper session manager with save/load capabilities:

session_class.py
from pydantic_ai import Agent, ModelMessage, ModelMessagesTypeAdapter
from pydantic_core import to_json
from pathlib import Path

class ChatSession:
    def __init__(self, agent: Agent, session_id: str):
        self.agent = agent
        self.session_id = session_id
        self.messages: list[ModelMessage] = []

    def chat(self, user_message: str) -> str:
        """Send a message and get response, maintaining history."""
        result = self.agent.run_sync(
            user_message,
            message_history=self.messages
        )
        # Update history with all messages
        self.messages = result.all_messages()
        return result.output

    def save(self, filepath: str):
        """Save session to JSON file."""
        data = to_json(self.messages)
        Path(filepath).write_bytes(data)

    def load(self, filepath: str):
        """Load session from JSON file."""
        data = Path(filepath).read_bytes()
        self.messages = ModelMessagesTypeAdapter.validate_json(data)

    def clear(self):
        """Clear conversation history."""
        self.messages = []

# Usage
agent = Agent('openai:gpt-4o')
session = ChatSession(agent, session_id='pablo_001')

# Have a conversation
print(session.chat("Hi, I'm Pablo"))
print(session.chat("What's my name?"))  # Will remember

# Save for later
session.save('/tmp/pablo_session.json')

# Later, load and continue
session.load('/tmp/pablo_session.json')
print(session.chat("Do you still remember me?"))  # Yes!

Stateful Tools

Tools can also read and write state. This is useful when you want the agent to explicitly save information.

stateful_tools.py
from dataclasses import dataclass, field
from pydantic_ai import Agent, RunContext

@dataclass
class UserState:
    name: str | None = None
    preferences: dict = field(default_factory=dict)

agent = Agent(
    'openai:gpt-4o',
    deps_type=UserState,
    instructions='You are a helpful assistant. Use tools to remember user information.'
)

@agent.tool
def save_name(ctx: RunContext[UserState], name: str) -> str:
    """Remember the user's name."""
    ctx.deps.name = name
    return f"Got it! I will remember your name is {name}"

@agent.tool
def save_preference(ctx: RunContext[UserState], key: str, value: str) -> str:
    """Save a user preference."""
    ctx.deps.preferences[key] = value
    return f"Saved: {key} = {value}"

@agent.tool
def get_info(ctx: RunContext[UserState]) -> dict:
    """Get all saved user information."""
    return {'name': ctx.deps.name, 'preferences': ctx.deps.preferences}

# Usage
state = UserState()
result1 = agent.run_sync("My name is Pablo and I prefer dark mode", deps=state)
result2 = agent.run_sync("What do you know about me?", deps=state, message_history=result1.new_messages())

# State persists outside the agent
print(f"State: {state}")
# UserState(name='Pablo', preferences={'theme': 'dark'})

Message History vs State

| Aspect | Message History | State (Dependencies) | |--------|-----------------|---------------------| | What it stores | Raw conversation | Structured data | | How LLM uses it | Sees full conversation | Accessed via tools | | Token cost | Grows with conversation | Fixed size | | Best for | Context, continuity | Explicit facts, settings |

Use message history for natural conversation flow. Use state for specific data you want to track explicitly.

Truncating Long History

Long conversations create problems: token costs increase, LLM context window has limits, old messages may not be relevant. Keep only recent messages:

truncate_history.py
from pydantic_ai import Agent, ModelMessage

def keep_recent(messages: list[ModelMessage], keep: int = 10) -> list[ModelMessage]:
    """Keep only the N most recent messages."""
    if len(messages) <= keep:
        return messages
    return messages[-keep:]

# Apply before each run
history = keep_recent(all_messages, keep=10)
result = agent.run_sync(message, message_history=history)
Key Takeaways
  • 1Agents are stateless by default. Each run is independent unless you pass history.
  • 2Message history = memory. Pass previous messages to maintain conversation context.
  • 3State in dependencies. Use dataclasses for structured data the agent should track.
  • 4Persist sessions. Save to files or database for continuity across restarts.
  • 5Process long histories. Truncate or summarize to manage token costs.