Module 1: Memory at Scale

HermesAgent Memory Storage

Learning objectives

  • Explain the core mental model behind HermesAgent Memory Storage
  • Apply HermesAgent Memory Storage within Memory at Scale
  • Identify important boundaries, trade-offs, and failure modes
  • Produce concrete evidence from the practice exercise
Framework-agnostic design reference from the vault

Related: Hermes Memory Overview | Hermes Memory Retrieval | Hermes Memory Optimization | Hermes Index

The choice of memory backend determines what your agent can remember, how fast it can recall, and what it costs at scale. This note compares the options and shows how to wire them up.


What a Backend Has to Do

A memory backend has four jobs:

  1. Store entries durably (text + metadata + embedding)
  2. Search by semantic similarity (vector ANN)
  3. Filter by metadata (tags, dates, source)
  4. Manage lifecycle — update, delete, decay

Different backends prioritize differently. Pick one whose strengths match your access pattern.

Engineering analogy: picking a memory backend is like picking a database. SQLite for simplicity, Postgres for scale, Redis for speed. There is no universally right answer; there's a right one for your workload.


The Four Backend Tiers

BackendCapacitySpeedOperational costWhen to use
In-memory dict< 10K entriesvery fasttrivialDemos, tests
SQLite + sqlite-vss< 1M entriesfastlowPersonal agents, embedded
Postgres + pgvector< 100M entriesfastmediumMost production agents
Dedicated vector DB (Qdrant, Weaviate, Pinecone)unlimitedvery fastmedium-highLarge scale, multi-tenant

Most projects start at SQLite, move to Postgres when they have real users, and only consider a dedicated vector DB when search latency becomes the bottleneck.


Tier 1: In-Memory

from hermes.memory import InMemory

memory = InMemory()
agent = Hermes(..., memory=memory)

Lives in the process. Disappears on exit. Useful for:

  • Tests (deterministic, fast)
  • Single-shot scripts
  • Demo agents

Don't use in production. The first restart wipes everything.


Tier 2: SQLite

from hermes.memory import SQLiteMemory

memory = SQLiteMemory(path="./data/memory.db",
                     embedding_model="text-embedding-3-small")

A real DB file. Persists across runs. Uses sqlite-vss for vector search. Pros:

  • Zero ops — it's a file
  • Plenty of capacity for personal agents
  • Works on every platform
  • Trivial to back up (copy the file)

Cons:

  • Single-writer; concurrent agents serialize through it
  • Doesn't shard; large indexes get slow

Use for personal projects, side projects, and pre-launch products.


Tier 3: Postgres + pgvector

from hermes.memory import PostgresMemory

memory = PostgresMemory(
    dsn=os.environ["DATABASE_URL"],
    embedding_model="text-embedding-3-small",
    table="agent_memory",
)

The recommended default for production. Pros:

  • Great for mixed workloads (vector + relational filters)
  • Mature operational story (backups, replicas, observability)
  • Handles concurrent agents
  • Scales to ~100M vectors with HNSW indexes
  • Familiar to existing teams

Cons:

  • Operational footprint (you have to run a Postgres)
  • HNSW index tuning needed at scale

For most agent products, this is the right answer.


Tier 4: Dedicated Vector DBs

When your retrieval is the hot path (high QPS, very large corpus, multi-tenancy):

  • Qdrant — open-source, fast, good filtering
  • Weaviate — strong hybrid search
  • Pinecone — managed, low ops
  • Milvus — large-scale, GPU-accelerated
from hermes.memory import QdrantMemory

memory = QdrantMemory(
    url=os.environ["QDRANT_URL"],
    api_key=os.environ["QDRANT_API_KEY"],
    collection="agent-memory",
    embedding_model="text-embedding-3-small",
)

Use when:

  • Memory queries are the bottleneck
  • You need >100M vectors
  • Multi-tenant isolation must be enforced at the storage layer
  • You're already running one for other purposes

For most teams, this tier is premature optimization. Start at Postgres.


What Gets Stored

A typical memory entry on disk:

CREATE TABLE agent_memory (
    id          uuid PRIMARY KEY,
    namespace   text NOT NULL,
    text        text NOT NULL,
    embedding   vector(1536),               -- pgvector type
    tags        text[],
    source      text,
    metadata    jsonb,
    created_at  timestamptz DEFAULT now(),
    last_used   timestamptz,
    use_count   int DEFAULT 0
);

CREATE INDEX ON agent_memory USING hnsw (embedding vector_cosine_ops);
CREATE INDEX ON agent_memory (namespace);
CREATE INDEX ON agent_memory USING gin(tags);

The HNSW index is what makes semantic search fast. Without it, search is a full-table scan.


Embedding Models

The embedding model determines vector dimension and quality. Choose once, stick with it — re-embedding a corpus is expensive.

ModelDimCostNotes
text-embedding-3-small1536$OpenAI; great default
text-embedding-3-large3072$$Higher quality; bigger storage
nomic-embed-text768free (local)Solid local option
cohere-embed-v31024$Good multilingual

For most projects, text-embedding-3-small is the right default. Local embedders work well too if you can run them.

memory = PostgresMemory(
    dsn=...,
    embedding_model="text-embedding-3-small",
)

See Hermes LLM Configuration.


Write Path

A write hits storage as follows:

memory.write(
    text="User prefers tabular output for data summaries.",
    tags=["preference", "format"],
    source="session_42",
    namespace="user_123",
)

Steps inside:

  1. Generate the embedding (one model call)
  2. Upsert into the table with metadata
  3. Optionally trigger summarization/compression jobs (see Hermes Memory Optimization)

Embeddings cost money. Don't write trivial entries — see Hermes Memory Overview §What Goes In.

Batch writes

For ingestion (loading a knowledge base), batch is essential:

memory.write_batch([
    {"text": "...", "tags": ["docs"]},
    {"text": "...", "tags": ["docs"]},
    ...
])

Hermes batches embedding generation, so 100 writes ≈ 1 model call (when the model supports it).


Namespacing and Multi-Tenancy

Namespaces partition memory:

mem_user_a = memory.namespace("user_a")
mem_user_b = memory.namespace("user_b")

mem_user_a.write("...")
mem_user_b.search("...")     # cannot see user_a's data

Implement as a column index (Postgres) or a collection (Qdrant). Either way, namespace is non-optional in any product with more than one user.


Backups and Migration

Memory is real data. Treat it like a database.

  • Back up Postgres regularly. SQLite: cp memory.db memory.db.bak from a quiesced state.
  • Migrate schema changes with proper migrations (Alembic, Sqitch).
  • Re-embed when you change the embedding model. This is a heavy job; plan for it.

A common production failure: someone changes the embedding model and the existing 5 million vectors are now in a different vector space. Search returns nonsense. Either re-embed or pin the model.


Hybrid Memory: Vector + Keyword

Pure vector search misses exact-match queries ("find the entry mentioning Bug-1234"). Hybrid stores augment vector search with keyword indexes:

memory = HybridMemory(
    vector=PostgresMemory(...),
    keyword=PostgresFTS(...),         # Postgres full-text search
    weight=0.7,                       # vector vs keyword weight
)

Hybrid retrieval ranks results by a weighted combination. See Hermes Memory Retrieval §Hybrid.


Cost Math

A back-of-envelope:

  • Embedding model: $0.02/1M tokens ≈ $0.0002 per typical entry
  • Storing 1M entries: ~10 GB of vectors at fp32, 1536-dim
  • Postgres on a small managed plan: ~$30/month
  • Search at QPS 10: well within Postgres' capacity

For a personal agent: pennies/month. For a product with 10K users each writing 100 facts: still tens of dollars/month. Memory is rarely the budget driver — model calls are. See Hermes Cost Optimization.


Common Mistakes

  • Storing the entire transcript — eventually unusable; summarize first
  • No namespace — cross-user contamination, eventually a privacy bug
  • Re-embedding without a plan — the great vector-space switcheroo
  • Choosing a vector DB too early — Postgres is enough for years
  • Synchronous writes in the hot path — embedding calls add latency; batch where you can
  • No decay or TTL — memory grows forever and retrieval rots
  • Embedding without normalizing text — case/whitespace differences become "different" entries

Related

  • Hermes Memory Overview — concept layer
  • Hermes Memory Retrieval — read path
  • Hermes Memory Optimization — pruning, summarization
  • Hermes LLM Configuration — embedding model choice
  • Hermes Cost Optimization — what memory actually costs
  • Hermes Index — full topic map

Practice lab

Implement the smallest runnable agent workflow that demonstrates HermesAgent Memory Storage. Trace inputs, state, model and tool calls, outputs, and cost; inject one failure and add a regression test that prevents it from returning. Add an operational constraint such as concurrency, recovery, security, latency, or cost, and defend the resulting design trade-off.

Review questions

  1. What problem does HermesAgent Memory Storage solve, and what assumptions does it rely on?
  2. Which boundary or failure case is easiest to miss, and how would you expose it?
  3. What alternative design would you consider, and what trade-off would change the decision?
  4. What artifact, trace, test, or metric proves that your implementation is correct?

Completion evidence

  • A working artifact, annotated trace, or reproducible experiment
  • At least one normal case and one deliberately failing or boundary case
  • A concise explanation of the design choice and its trade-offs
  • Saved output showing how correctness was evaluated