Pydantic AI Memory
One capability in the Agent constructor gives every run recalled context and automatic turn persistence — through Pydantic AI’s own capability channels, so memory never pollutes your message history and a failed run never leaves a half-remembered turn. Plus five agent tools, typed search results, and async helpers.
What the integration provides
| Layer | What it does |
|---|---|
MemorySyncCapability | Recall injected through the framework’s instructions channel before every run; completed turns persisted in after_run. One entry in Agent(capabilities=[...]). |
| Agent tools | create_memorysync_tools() — five structured tools (add, search, list, update, delete) the model can call, which never raise. |
create_memory_search_tool | Memory search returning typed list[MemoryResult] Pydantic models instead of prose. |
| Helpers | get_memory_context, search_memories, save_turn — async, for hand-wired setups. |
| Mem0 | Supermemory | Zep | MemorySync | |
|---|---|---|---|---|
| Pydantic AI integration exists | ✗ nothing shipped | ✗ nothing shipped | △ zep-pydantic-ai | ✓ pydantic-ai-memorysync |
| Compatible with pydantic-ai v2 | — | — | ✗ pins pydantic-ai <2 | ✓ built on the v2 capability system |
| Failed runs leave no half-turns | — | — | ✗ persists user turn mid-run | ✓ success-only by construction |
| Replay-safe injection | — | — | ✗ mutates message history | ✓ instructions channel, never a message part |
| Typed tool results | — | — | ✗ | ✓ list[MemoryResult] |
Install
pip install pydantic-ai-memorysync
Set MEMORYSYNC_API_KEY in the environment, or pass api_key explicitly. Requires pydantic-ai (or pydantic-ai-slim) 2.x (Python 3.10+). This is a Python surface — for TypeScript agents use the Vercel AI SDK or Mastra integrations.
The capability
from pydantic_ai import Agentfrom pydantic_ai_memorysync import MemorySyncCapabilityagent = Agent("openai:gpt-5",instructions="You are a helpful assistant.",capabilities=[MemorySyncCapability(user_id="customer-7")],)result = await agent.run("What should I cook tonight?")# The model saw:# You are a helpful assistant.## Relevant memories about this user from previous conversations:# - human: I'm vegetarian and I fly aisle.## ...and this exchange is now remembered for every future run.
Recall rides the instructions channel (ModelRequest.instructions) — visible to the model, never a conversation part. Replaying result.all_messages() into the next run can never stack stale memory blocks into the transcript, the classic failure mode of message-mutation integrations. Tool calls, retry prompts and thinking parts never persist; streaming runs persist the final streamed text. Turns are grouped by the run’s conversation_id, which the framework carries across message_history continuations — pass session_id="..." to pin the grouping yourself. persist=False gives read-only memory.
Identity from your deps
from dataclasses import dataclassfrom pydantic_ai import Agentfrom pydantic_ai_memorysync import MemorySyncCapability@dataclassclass MyDeps:user_id: stragent = Agent("openai:gpt-5",deps_type=MyDeps,capabilities=[MemorySyncCapability()], # reads ctx.deps.user_id)await agent.run("hi", deps=MyDeps(user_id="customer-7"))# Or resolve it yourself, from anything on your deps:MemorySyncCapability(user_id_resolver=lambda ctx: ctx.deps.user_id)
Resolution order: user_id_resolver(ctx) when provided, then the static user_id, then a user_id attribute on ctx.deps. Without an identity the run proceeds memoryless and the miss is reported through on_error — guessing a shared namespace would silently mix users’ memories, the one unforgivable failure for a memory layer.
Agent memory tools
from pydantic_ai import Agentfrom pydantic_ai_memorysync import create_memorysync_toolsagent = Agent("openai:gpt-5",instructions="Use the memory tools to remember durable facts.",tools=create_memorysync_tools(user_id="customer-7"),)await agent.run("Remember that I prefer aisle seats.")# Untrusted agents: search + list only.create_memorysync_tools(user_id="customer-7", read_only=True)
| Tool | What it does | Failure behaviour |
|---|---|---|
add_memory | Save one durable fact; duplicate saves answer “already stored”. | Readable error string — never raises. |
search_memory | Semantic search with relevance scores. | Readable error string. |
list_memories | Newest-first listing. | Readable error string. |
update_memory | Change tags/importance. Memory text is immutable. | Readable error string. |
delete_memory | Permanent delete by id, scoped to the configured user. | Readable error string. |
Same five operations, same response strings as the LangChain, AI SDK, CrewAI, Mastra, OpenAI Agents, LlamaIndex and Google ADK tool sets — an agent moved between frameworks keeps behaving the same way. In Pydantic AI a tool exception fails the whole run, so these tools return short readable strings instead — a memory lookup is never worth a dead run.
Typed search results
from pydantic_ai import Agentfrom pydantic_ai_memorysync import MemoryResult, create_memory_search_toolagent = Agent("openai:gpt-5",tools=[create_memory_search_tool(user_id="customer-7")],)# The tool returns list[MemoryResult] — validated Pydantic models:# MemoryResult(id="m_123", text="human: I'm vegetarian", score=0.62)# For agents whose downstream code consumes tool results structurally.
Standalone helpers
from pydantic_ai_memorysync import (get_memory_context,save_turn,search_memories,)# 1. Prompt-ready context block ("" for a new user)context = await get_memory_context("what should I cook?", user_id="customer-7")# 2. Scored raw resultshits = await search_memories("dietary preferences", user_id="customer-7")# 3. Explicit persistence — RAISES on failure (an explicit call is# owed the truth), unlike the capability's degrading planes.await save_turn(user_id="customer-7",user="I'm vegetarian",assistant="Noted!",session_id="thread-42",)
Public API
| Export | Kind | Notes |
|---|---|---|
MemorySyncCapability | AbstractCapability | user_id, user_id_resolver, k, template, persist, session_id, on_error — all optional; identity falls back to ctx.deps.user_id. |
create_memorysync_tools | Tool factory | Five plain async callables; read_only=True returns search + list only. |
create_memory_search_tool | Tool factory | Returns typed list[MemoryResult]; failures return [] and report through on_error. |
MemoryResult | Pydantic model | id, text, score — the typed search hit. |
get_memory_context / search_memories / save_turn | Async helpers | save_turn raises on failure; all share the capability’s seeds. |
MemorySyncAPIError | Exception | Carries the HTTP status and server detail. |
Supported versions
| Package | Registry | Requires | Runtime |
|---|---|---|---|
pydantic-ai-memorysync 1.0.0 | PyPI | pydantic-ai (or -slim) >=2 <3 | Python 3.10+ |
The test suite drives REAL agents (TestModel/FunctionModel) — instruction-channel injection and its replay safety, one-recall-per-run caching, success-only persistence, streaming, typed tool results — and CI re-runs it against the latest pydantic-ai 2.x release on every push, so a capability-system change upstream fails our pipeline before it can fail your agent.