Add memory and RAG to an AI agent with Postgres and pgvector
Agent memory is three different things — thread state, long-term facts, and retrieval. You can run all three in one Postgres database. Here's how, with LangGraph checkpoints and pgvector, on a database your agent gets at deploy time.
"Add memory to my agent" usually hides three separate problems:
- Thread state — the agent should pick up a conversation where it left off, even after a restart or a redeploy.
- Long-term facts — things the agent learned about a user or task that should outlive a single thread.
- Retrieval (RAG) — pull the right chunk of your documents or past records into context when a question comes in.
Most stacks solve these with three different systems: a checkpoint store, a key-value or document store, and a vector database. Each one is another credential, another bill, another thing to back up, and another place data can leak from.
You don't need three. Plain Postgres with the pgvector extension handles all of them — and on a2a cloud, every agent already gets its own managed Postgres at deploy time. This post walks through the pattern.
Step 0: the database
Declare a database in a2a.yaml and it is provisioned for the agent and injected as DATABASE_URL:
resources:
databases:
- name: app
engine: postgres
access_mode: read_write
env:
url: DATABASE_URLThe database belongs to that agent, is scoped to that agent's role, and is created and torn down with it. (Full background: Give your AI agent a Postgres database.)
1. Thread state: LangGraph checkpoints in Postgres
LangGraph's checkpointer interface is exactly the right abstraction for thread state. Point PostgresSaver at the agent's database and every step of every thread is persisted:
import os
from langgraph.checkpoint.postgres import PostgresSaver
with PostgresSaver.from_conn_string(os.environ["DATABASE_URL"]) as checkpointer:
checkpointer.setup() # creates checkpoint tables once
graph = builder.compile(checkpointer=checkpointer)
graph.invoke(
{"messages": [("user", "Where did we leave the migration plan?")]},
config={"configurable": {"thread_id": "customer-42"}},
)The in-memory MemorySaver from the tutorials is fine on a laptop and wrong in production: the moment your process restarts, every conversation is gone. With Postgres, a redeploy is invisible to the user.
If you're on CrewAI or the OpenAI Agents SDK the idea is identical — persist sessions to DATABASE_URL instead of process memory.
2. Long-term facts: just a table
Long-term memory is usually over-engineered. Start with a table:
create table if not exists memories (
id bigserial primary key,
subject text not null, -- user id, account, project
fact text not null,
source text, -- which run or tool produced it
created_at timestamptz default now()
);
create index on memories (subject);The agent writes facts through a tool ("remember that this customer deploys on Fridays only") and reads them back by subject at the start of a thread. Because it's relational, you can also *delete* a fact — which matters the first time a user asks you to forget something.
3. Retrieval: pgvector in the same database
Now add embeddings. Enable the extension and give chunks a vector column:
create extension if not exists vector;
create table if not exists chunks (
id bigserial primary key,
doc_id text not null,
content text not null,
embedding vector(1536) not null
);
create index on chunks using hnsw (embedding vector_cosine_ops);Retrieval is a single query:
import os
import psycopg
from pgvector.psycopg import register_vector
def retrieve(query_embedding, k: int = 5):
with psycopg.connect(os.environ["DATABASE_URL"]) as conn:
register_vector(conn)
return conn.execute(
"select doc_id, content from chunks "
"order by embedding <=> %s limit %s",
(query_embedding, k),
).fetchall()Wrap retrieve as a tool, call your embedding model of choice to produce query_embedding, and the agent has RAG.
Why one database beats three systems
Joins instead of glue code. The real questions agents get are hybrid: *"what did we tell this customer about pricing last month?"* That's a vector search filtered by subject and a date range. In one database it's one query with a where clause. Across a vector DB and a relational DB it's two round-trips and a merge in Python.
One blast radius. A prompt-injected agent can only reach the database its role was provisioned for. With three external services, you're managing three sets of credentials inside the agent's environment. (More on the security model: Give AI agents database access without handing over production.)
Forgetting is a `delete`. Deletion requests and retention policies are much easier when memory, facts, and embeddings live side by side and share keys.
Memory you can audit. Memory is where an agent's behavior quietly changes over time — a stored "fact" steers every future answer. On a2a cloud each run emits a signed Ed25519 receipt, so when a memory write turns out to be wrong you can answer *which run wrote it, and under whose authority* instead of grepping logs. See signed receipts vs trace logs.
When to reach for something bigger
Postgres + pgvector comfortably covers the memory needs of most agents. Reach for a dedicated vector or graph system when you have very large corpora with heavy concurrent retrieval, or when you need structure-aware recall across relationships — we wrote up an early investigation into graph-shaped dynamic memory for that end of the spectrum. Start simple; you can always move the chunks table later.
Expose memory to other agents
Once remember and retrieve are tools, they're also exposed over the agent's MCP server — so an IDE client or another agent can query the same memory without a second integration. See wiring an MCP server to your agent.
Try it
Start from a framework quickstart, then add the three tables above:
- Deploy a LangGraph agent in 60 seconds
- Deploy a CrewAI agent in 60 seconds
- Deploy an OpenAI Agents SDK agent in 60 seconds
pip install a2a-pack
a2a deployDATABASE_URL is already there. Your agent's memory can be too.