Pydantic AI
Give a Pydantic AI agent memory of its users.
recall-pydantic-ai
starts every run with what the agent knows about the user, and gives the model a
remember tool that stores new facts in Recall.
Facts are typed: each one is a kind of fact from a fixed list, with a value. So when a user says "actually, call me Sam", the new name replaces the old one instead of sitting next to it, and the old one stays in the history. Reading memory needs no second model and no embedding service.
Install and use
pip install recall-pydantic-ai "pydantic-ai-slim[anthropic]"
recall-pydantic-ai also installs the recall binary and the
polign_db server for Linux, macOS and Windows, so there is no separate database to download.
The anthropic extra is only for the model in this example.
from dataclasses import dataclass from polign_recall import Client from pydantic_ai import Agent from recall_pydantic_ai import RecallMemory @dataclass class Deps: user_id: str recall = Client(local_dir="./recall-data", typed=True) # one per process agent = Agent( "anthropic:claude-sonnet-5-5", deps_type=Deps, instructions="You help engineers with their projects.", capabilities=[RecallMemory(recall, subject=lambda ctx: ctx.deps.user_id)], ) agent.run_sync("I'm Alex. We deploy to Fly.io.", deps=Deps("user-42")) result = agent.run_sync("Where do we deploy again?", deps=Deps("user-42")) print(result.output) # You deploy to Fly.io.
The second run gets no chat history. It answers from memory: the first run stored
name and deployment_target through the remember tool,
and the second run started with both in its instructions.
What happens in a run
- When the run starts,
RecallMemoryloads the user's current facts and adds them to the instructions in a<recall_memory>block. The block stays the same for the whole run, so the model provider's prompt cache keeps working. - When the user states a fact, the model calls
rememberwith the kind of fact and its value. A new value for a single-valued kind replaces the old one, and the tool tells the model what changed:Updated name to Sam; it was Alex. - The next run sees the updated facts. The replaced value is still in
the record:
recall.history("user-42", "name")lists both. - When a user has many facts, more than
limit(20 by default), the run also searches memory for the prompt, adds what it finds to the block, and gives the model asearch_memorytool.
subject is a fixed string or a function of the run context. Use a stable
identifier that comes from your sign-in, never a session or conversation id.
The tools
| Tool | When the model gets it | What it does |
|---|---|---|
remember | By default | Stores one fact. The list of allowed kinds comes from Recall's registry, so the model cannot invent new ones. |
forget | With forget_tool=True | Withdraws a fact, or every value of one kind, when the user asks. The record of the change is kept. |
search_memory | When the user has more than limit facts | Searches the facts that did not fit in the instructions. |
Options
| Option | Default | What it does |
|---|---|---|
client | required | A polign_recall.Client. Create one per process and share it. |
subject | required | The user, as a string or lambda ctx: ... |
limit | 20 | Facts loaded into the instructions. |
who | "the user" | How the instructions and tool descriptions refer to the person. |
template | <recall_memory> block | Wrapper around the facts; must contain {context}. |
remember_tool | on | Off makes the memory read-only. |
forget_tool | off | Lets users withdraw a fact by asking. |
search_when_overflowed | on | Search for the prompt when the user has more than limit facts. |
read_timeout, write_timeout | 2 s, 10 s | How long to wait for Recall before giving up. |
Your own kinds of facts
The built-in registry covers names, preferences, project facts and free-form notes. To use your own, write it as JSON and point the client at it:
{
"name": { "cardinality": "single", "value_type": "string", "description": "The name the user asks to be called" },
"plan_tier": { "cardinality": "single", "value_type": "string", "description": "The user's subscription plan" },
"open_issue": { "cardinality": "multi", "value_type": "string", "description": "A problem the user reported that is not resolved yet" }
}
recall = Client(local_dir="./recall-data", env={"POLIGN_PREDICATES": "registry.json"}, typed=True)
single means a newer value replaces the old one. multi means
each value is one more fact. value_type is string,
number, or boolean, and the tool converts what the model sends:
"1,200" becomes 1200 and "yes" becomes true. Your file replaces the built-in kinds, except
note, which Recall always keeps for statements nothing else fits.
Where the memory lives
Client(local_dir="./recall-data", typed=True) keeps the memory on this machine. typed=True asks for the typed tools this capability files beliefs with (recall 0.18 or later). The first
process to open the directory starts a polign_db server for it in the background, listening
only on localhost, and other processes share it. The server keeps running after your program
exits; its process id is in recall-data/runtime.json.
Agents on several machines need one shared memory. Run recall serve where
they can all reach it, storing into your S3, GCS or Azure bucket
(Run polign_db shows how), and connect to it:
recall = Client(url="https://memory.internal:8443", api_key=os.environ["RECALL_API_KEY"], typed=True) # a recall serve started with -typed
Client.from_env() reads RECALL_URL and
RECALL_API_KEY, and starts a local process when RECALL_URL is not
set, so the same code runs on a laptop and in production.
When Recall is down
- Reading fails open. If Recall is slow or down when a run starts, the run goes on without the remembered facts and a warning is logged.
- Writing reports the failure to the model as the tool's result, such
as
The fact was not saved: ..., so it can tell the user. - A kind that is not in the registry, or a value of the wrong type, asks the model to try again.
Limits
- It uses Recall's typed memory. A client opened with
text=Trueis not supported yet. - It has not been tested under Pydantic AI's durable execution (Temporal, DBOS).
Agent.from_speccannot create it, because it takes a live client.- It needs Python 3.10 or later and
pydantic-ai-slim2.16 or later in 2.x.
Source & license
recall-pydantic-ai is open source under the Apache License 2.0 and lives next
to the Python client in
github.com/Polign/polign.
Bug reports and pull requests go there.