# Add long-term memory to a Pydantic AI agent (one capability, per-user recall across runs)

*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](https://ai.pydantic.dev/message-history/)). 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](https://ai.pydantic.dev/message-history/) and [capabilities](https://ai.pydantic.dev/capabilities/). Everything about MemorySync comes from its [Pydantic AI Memory guide](https://docs.memorysync.io/guides/pydantic-ai?utm_source=hashnode&utm_medium=social&utm_campaign=pivot_oct19&utm_content=pydantic-ai-agent-memory-across-runs). 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

1. One `MemorySyncCapability` in the `Agent` constructor that recalls relevant facts before every run and sends the user's prompt for fact extraction after a successful run.
2. User identity taken from your `deps`, so one agent can serve many users.
3. Optional: five memory tools, a typed search tool and async helpers.

## Install

```bash
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](https://ai.pydantic.dev/capabilities/)). MemorySync ships memory as one:

```python
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 as `session_id`). Pass `session_id="..."` to pin it yourself. `persist=False` gives 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`:

```python
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:

```python
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`:

```python
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:

```python
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_error` output. 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_id` must be identical across runs. `session_id` / `conversation_id` can 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_context` output into your prompt.
- **Version errors.** The package targets `pydantic-ai` 2.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](https://ai.pydantic.dev/message-history/)).

**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](https://ai.pydantic.dev/message-history/)).

**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](https://ai.pydantic.dev/message-history/)).

**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](https://docs.memorysync.io/guides/pydantic-ai?utm_source=hashnode&utm_medium=social&utm_campaign=pivot_oct19&utm_content=pydantic-ai-agent-memory-across-runs)).

## 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](https://memorysync.io/try?utm_source=hashnode&utm_medium=social&utm_campaign=pivot_oct19&utm_content=pydantic-ai-agent-memory-across-runs).
