Add long-term memory to a Pydantic AI agent (one capability, per-user recall across runs)
message_history only carries one conversation. Here's how to give a Pydantic AI 2.x agent per-user memory across runs and threads with one capability, plus five memory tools and typed search.
Disclosure: I'm Mohammed, founder of MemorySync. I drafted this article with AI assistance and reviewed it for accuracy against the official MemorySync and Pydantic AI docs. The code is adapted from the docs and I haven't run it.
Pydantic AI keeps context across runs through message history: you pass message_history= (or the previous result's conversation) into the next Agent.run, and result.all_messages() / new_messages() give you the messages back (Pydantic AI message history docs). Storing and reloading that list is up to you, and it only covers one conversation. Start a new thread and the agent knows nothing about the user it talked to yesterday.
This post adds per-user long-term memory with MemorySync's Pydantic AI integration, pydantic-ai-memorysync. MemorySync is a hosted memory layer, and I'm building it. Pydantic AI behaviour comes from its official docs on message history and capabilities. Everything about MemorySync comes from its Pydantic AI Memory guide. The code is adapted from those docs. I haven't run it for this post, so check it against your versions.
What you'll set up
- One
MemorySyncCapabilityin theAgentconstructor that recalls relevant facts before every run and sends the user's prompt for fact extraction after a successful run. - User identity taken from your
deps, so one agent can serve many users. - Optional: five memory tools, a typed search tool and async helpers.
Install
pip install pydantic-ai-memorysync
Per the guide it needs pydantic-ai (or pydantic-ai-slim) 2.0+ and Python 3.10+. PyPI currently lists 1.1.0 with pydantic-ai-slim>=2,<3. Set MEMORYSYNC_API_KEY in the environment, or pass api_key, and keep it server-side. This is Python only; the guide points TypeScript users to the Vercel AI SDK or Mastra integrations.
Step 1: Add the capability
In Pydantic AI, a capability is a reusable unit of agent behaviour that you pass to the Agent constructor instead of wiring instructions, tools and hooks separately (capabilities docs). MemorySync ships memory as one:
from pydantic_ai import Agent
from pydantic_ai_memorysync import MemorySyncCapability
agent = Agent(
"openai:your-model",
instructions="You are a helpful assistant.",
capabilities=[MemorySyncCapability(user_id="customer-7")],
)
# First run
await agent.run("I'm vegetarian and I fly aisle.")
# A later run, new conversation, same user
result = await agent.run("What should I cook tonight?")
What happens, per the guide:
- Recall goes through the instructions channel, not into the message list. The model sees a "Relevant memories about this user" block after your instructions, but it is never a conversation part. So if you replay
result.all_messages()into the next run, old memory blocks don't pile up in the transcript. - Capture happens in
after_run, which Pydantic AI fires only when a run succeeds. A failed run leaves no half-remembered turn. - Only the user's prompt is sent. MemorySync extracts the durable facts in it and stores those; filler like "ok" or "thanks" yields none. The model's reply, tool calls, retry prompts and thinking parts aren't sent. Streaming runs send the prompt once the stream completes.
- Prompts are grouped by the run's
conversation_id(sent assession_id). Passsession_id="..."to pin it yourself.persist=Falsegives read-only memory.
Failure handling: if recall fails, the agent runs with its base instructions; if the send fails, the run result is untouched. Both report through on_error (a logging warning by default).
Step 2: Take the user from your deps
One agent usually serves many users, so don't hard-code user_id:
from dataclasses import dataclass
from pydantic_ai import Agent
from pydantic_ai_memorysync import MemorySyncCapability
@dataclass
class MyDeps:
user_id: str
agent = Agent(
"openai:your-model",
deps_type=MyDeps,
capabilities=[MemorySyncCapability()], # reads ctx.deps.user_id
)
await agent.run("hi", deps=MyDeps(user_id="customer-7"))
# Or resolve it from anything on your deps:
MemorySyncCapability(user_id_resolver=lambda ctx: ctx.deps.user_id)
The resolution order is user_id_resolver(ctx), then a static user_id, then ctx.deps.user_id. If none is found, the run goes ahead without memory and the miss is reported through on_error; it never falls back to a shared namespace. My advice: fill user_id from your authenticated user record, not from the request body.
Keep using message_history for the current conversation. The capability adds what Pydantic AI doesn't do on its own: facts about the user that carry over to new threads.
Step 3 (optional): Memory tools
If you want the model to decide what to save:
from pydantic_ai import Agent
from pydantic_ai_memorysync import create_memorysync_tools
agent = Agent(
"openai:your-model",
instructions="Use the memory tools to remember durable facts.",
tools=create_memorysync_tools(user_id="customer-7"),
)
# For agents you don't fully trust: search and list only
create_memorysync_tools(user_id="customer-7", read_only=True)
The tools are add_memory, search_memory, list_memories, update_memory (tags and importance only; memory text can't be changed) and delete_memory. In Pydantic AI a tool exception fails the whole run, so these return short readable error strings instead of raising.
Step 4 (optional): Typed search and helpers
When downstream code consumes tool output structurally, use the typed search tool. It returns list[MemoryResult] (Pydantic models with id, text, score), and on failure returns [] and reports through on_error:
from pydantic_ai import Agent
from pydantic_ai_memorysync import create_memory_search_tool
agent = Agent(
"openai:your-model",
tools=[create_memory_search_tool(user_id="customer-7")],
)
For hand-wired setups there are async helpers:
from pydantic_ai_memorysync import get_memory_context, save_turn, search_memories
context = await get_memory_context("what should I cook?", user_id="customer-7") # "" for a new user
hits = await search_memories("dietary preferences", user_id="customer-7")
# Explicit persistence. This one raises on failure.
await save_turn(user_id="customer-7", user="I'm vegetarian",
assistant="Noted!", session_id="thread-42")
save_turn sends only the user message. API errors raise MemorySyncAPIError, which carries the HTTP status and server detail.
Troubleshooting
- Nothing is remembered. Check
on_erroroutput. A run with no resolvable user proceeds memoryless; so does a failed run, because capture only happens after success. - A new thread knows nothing. The
user_idmust be identical across runs.session_id/conversation_idcan differ. - Duplicate memory text in the transcript. It shouldn't happen with the capability, since recall rides the instructions channel. If you see it, check you're not also pasting
get_memory_contextoutput into your prompt. - Version errors. The package targets
pydantic-ai2.x.
Common questions
Does Pydantic AI keep memory between runs?
Not on its own. Each Agent.run only sees the messages you pass in. To continue a conversation, pass the earlier messages back with message_history=, using result.all_messages() (everything, including prior runs) or result.new_messages() (only the current run) (Pydantic AI message history docs).
How do I persist message_history to a database?
The docs' intended route is a TypeAdapter: ModelMessagesTypeAdapter serializes messages to JSON and back. For a conversation you plan to continue, they suggest storing its Conversation (via ConversationTypeAdapter) rather than the messages alone. Where those bytes live and when to reload them is left to your application; conversation_id is the key to store them under, and the resolved value is on the result as AgentRunResult.conversation_id (message history docs).
How do I stop message history from growing too large?
Use the ProcessHistory capability. It lets you change the history before each model request, for example to keep only recent messages or summarize old ones (message history docs).
How do I make an agent remember a user across new conversations?
A stored history only covers the conversation it belongs to. For facts that should follow a user into new threads, you need a separate memory store. The setup in this post uses MemorySync's MemorySyncCapability: it adds recalled facts through the instructions channel, so nothing is added to your message history, and it sends the user's prompt for fact extraction only after a run succeeds (MemorySync Pydantic AI guide).
Sources
- MemorySync Pydantic AI Memory guide: docs.memorysync.io/guides/pydantic-ai
- Pydantic AI docs: ai.pydantic.dev/message-history/ and ai.pydantic.dev/capabilities/
- PyPI: pydantic-ai-memorysync 1.1.0
Try it
If you want to see the save, reload and recall loop before wiring up an agent, the MemorySync playground runs it in your browser: memorysync.io/try.
About the author
- Building MemorySync (memorysync.io) — persistent memory infrastructure for AI agents, Cursor, and Claude Code.
Comments
Join the discussionNo comments yet. Be the first to comment.
