Memory Adapters
Choose and configure memory adapters for agent persistence — InMemory, Redis, Postgres, SQLite, MongoDB, and Qdrant.
Adapter Factory
The simplest way to create a conversation adapter is the factory function. It accepts a config object and returns the matching adapter, loading its driver lazily:
import { createMemoryAdapter, unwrap } from '@cogitator-ai/memory';
const memory = await createMemoryAdapter({ provider: 'sqlite', path: './memory.db' });
unwrap(await memory.connect());createMemoryAdapter handles memory, redis, postgres, sqlite and mongodb and returns the common MemoryAdapter interface. For Qdrant use createEmbeddingAdapter({ provider: 'qdrant', dimensions }), which returns a ConnectableEmbeddingAdapter (an EmbeddingAdapter with connect() and disconnect()) to connect before use. Construct the class directly (e.g. new PostgresAdapter(...)) when you need its extra capabilities — facts, vector or keyword search.
All conversation adapters share the same MemoryAdapter interface: createThread, getThread, updateThread, deleteThread, addEntry, getEntries, getEntry, deleteEntry, clearThread, connect and disconnect. Some adapters add FactAdapter, EmbeddingAdapter or KeywordSearchAdapter.
Every method — connect() included — returns a MemoryResult instead of throwing, so a failed connection goes unnoticed unless you check it. Wrap calls in unwrap() (throws on failure) or check result.success; see Results and unwrap().
Calling createThread with an existing thread id is an upsert in every adapter: stored entries and the stored createdAt are kept, only the agent id and metadata change. updateThread merges the given metadata into the existing one.
InMemory
Zero-dependency in-process storage. Data is lost when the process exits — and when you call disconnect().
import { InMemoryAdapter, unwrap } from '@cogitator-ai/memory';
const memory = new InMemoryAdapter({ provider: 'memory', maxEntries: 5000 });
await memory.connect();
const thread = unwrap(await memory.createThread('agent-1'));
await memory.addEntry({
threadId: thread.id,
message: { role: 'user', content: 'Hello' },
tokenCount: 2,
});
console.log(memory.stats); // { threads: 1, entries: 1 }| Option | Type | Default | Description |
|---|---|---|---|
maxEntries | number | 10000 | Max entries across all threads; oldest are evicted |
When to use: Unit tests, prototyping, ephemeral agents that don't need persistence.
Redis
Short-term memory with automatic TTL expiration. Uses sorted sets for ordered retrieval. Supports both standalone Redis and Redis Cluster.
import { RedisAdapter, unwrap } from '@cogitator-ai/memory';
const memory = new RedisAdapter({
provider: 'redis',
url: 'redis://localhost:6379',
keyPrefix: 'myapp:',
ttl: 3600,
});
unwrap(await memory.connect());For Redis Cluster:
const memory = new RedisAdapter({
provider: 'redis',
cluster: {
nodes: [
{ host: 'redis-1', port: 6379 },
{ host: 'redis-2', port: 6379 },
],
scaleReads: 'slave',
},
password: process.env.REDIS_PASSWORD,
});| Option | Type | Default | Description |
|---|---|---|---|
url | string | — | Redis URL for standalone mode |
host | string | — | Host (alternative to url) |
port | number | — | Port (alternative to url) |
cluster | object | — | nodes and scaleReads (master/slave/all) |
keyPrefix | string | cogitator: or {cogitator}: for cluster | Key prefix for all Redis keys |
ttl | number | 86400 | TTL in seconds (24h default) |
password | string | — | Redis authentication password |
Each write refreshes the TTL of the thread and its entry index, so active conversations do not expire mid-way; expired entries are pruned from the index on read. In cluster mode a custom keyPrefix without a hash tag is wrapped automatically ('myapp:' → '{myapp}:') so all keys of the adapter map to one slot.
Requires: pnpm add ioredis
When to use: Ephemeral conversations that should auto-expire, high-throughput scenarios, or when you already run Redis.
Postgres
Full-featured adapter with threads, entries, facts, vector embeddings (pgvector), and full-text keyword search.
import { PostgresAdapter, unwrap } from '@cogitator-ai/memory';
const memory = new PostgresAdapter({
provider: 'postgres',
connectionString: 'postgresql://user:pass@localhost:5432/cogitator',
schema: 'cogitator',
poolSize: 10,
});
memory.setVectorDimensions(1536); // before connect(); default 768
unwrap(await memory.connect());On connect() the adapter creates the schema, the threads, entries, facts and embeddings tables, the pgvector extension, an HNSW cosine index on the vectors and a generated English tsvector column for keyword search. If the extension or an index cannot be created, it logs a warning and carries on.
HNSW needs no training data, so the index built on the empty table finds every row as the table fills up. Earlier versions built an ivfflat index there instead, which missed rows for small limits. The first connect() after upgrading replaces that index with HNSW, which on a large table takes a while. To avoid the wait, build the index ahead of the deploy with CREATE INDEX CONCURRENTLY idx_embeddings_vector_new ON <schema>.embeddings USING hnsw (vector vector_cosine_ops), then drop the old index and rename the new one to idx_embeddings_vector. An existing HNSW index is kept as it is.
search() sizes each query for the index: it raises hnsw.ef_search to the requested limit (up to 1000) and, on pgvector 0.8 or later, turns on iterative scans, so a large limit or a filter on agent, thread or user still gets every matching row. Equally similar rows are ordered by id, so the result does not depend on the query plan.
The embeddings.vector column is created with the dimension set at that moment, so call setVectorDimensions(n) before connect() — it throws once connected. An existing table keeps its original dimension.
| Option | Type | Default | Description |
|---|---|---|---|
connectionString | string | — | Postgres connection URL |
schema | string | cogitator | Schema name (letters, digits and underscores only) |
poolSize | number | 10 | Connection pool size |
Facts (FactAdapter)
const fact = unwrap(
await memory.addFact({
agentId: 'agent-1',
content: 'User timezone is UTC+3',
category: 'preference',
confidence: 1.0,
source: 'user',
metadata: { userId: 'alice' },
})
);
const preferences = unwrap(await memory.getFacts('agent-1', 'preference'));
const matches = unwrap(await memory.searchFacts('agent-1', 'timezone'));
await memory.updateFact(fact.id, { confidence: 0.8, expiresAt: new Date('2027-01-01') });
await memory.deleteFact(fact.id);getFacts and searchFacts (case-insensitive substring match) skip expired facts and sort by confidence.
Vector Search (EmbeddingAdapter)
await memory.addEmbedding({
sourceId: 'doc-1',
sourceType: 'document',
vector: embedding,
content: 'Cogitator supports multiple LLM backends.',
metadata: { agentId: 'agent-1' },
});
const results = unwrap(
await memory.search({
vector: queryVector,
limit: 5,
threshold: 0.7,
filter: { sourceType: 'document', agentId: 'agent-1' },
})
);search() needs a vector — embed the query with an embedding service first. limit defaults to 10 and threshold (cosine similarity) to 0.7. filter.agentId and filter.threadId match metadata.agentId / metadata.threadId; filter.userId matches metadata.userId and also lets through embeddings without a user. Remove embeddings with deleteEmbedding(id) or deleteBySource(sourceId).
Keyword Search (KeywordSearchAdapter)
Uses PostgreSQL full-text search over the generated tsvector column, ranked by ts_rank:
const hits = unwrap(
await memory.keywordSearch({
query: 'LLM backends',
limit: 10,
filter: { sourceType: 'document' },
})
);Requires: pnpm add pg and the pgvector extension for embeddings.
When to use: Production deployments. This is the most capable adapter.
SQLite
Local file-based persistence using better-sqlite3. Supports WAL mode for better concurrent read performance.
import { SQLiteAdapter, unwrap } from '@cogitator-ai/memory';
const memory = new SQLiteAdapter({
provider: 'sqlite',
path: './data/memory.db',
walMode: true,
});
unwrap(await memory.connect());Use :memory: for an in-memory SQLite database (useful for testing with SQL semantics):
const memory = new SQLiteAdapter({ provider: 'sqlite', path: ':memory:' });| Option | Type | Default | Description |
|---|---|---|---|
path | string | — | Path to .db file, or :memory: |
walMode | boolean | true | Enable WAL journal mode (disabled for :memory:) |
connect() returns a failure (rather than throwing) when better-sqlite3 is not installed. For a knowledge graph in the same kind of file, see SQLiteGraphAdapter.
Requires: pnpm add better-sqlite3
When to use: Desktop apps, CLI tools, single-process servers.
MongoDB
Document-oriented memory storage. Threads and entries are stored in the <prefix>threads and <prefix>entries collections, indexed on agentId and (threadId, createdAt).
import { MongoDBAdapter, unwrap } from '@cogitator-ai/memory';
const memory = new MongoDBAdapter({
provider: 'mongodb',
uri: 'mongodb://localhost:27017',
database: 'cogitator',
collectionPrefix: 'memory_',
});
unwrap(await memory.connect());| Option | Type | Default | Description |
|---|---|---|---|
uri | string | — | MongoDB connection URI |
database | string | cogitator | Database name |
collectionPrefix | string | memory_ | Prefix for collections |
Fields an entry or a thread leaves undefined are left out of the stored document rather than stored as null, so entries read back the same as from the other stores. A failed connect() closes its client and leaves the adapter disconnected, so calling connect() again retries. The adapter is checked against MongoDB 7.0 with the mongodb 7 driver.
Requires: pnpm add mongodb
When to use: When your stack already uses MongoDB, or when you need flexible document schemas.
Qdrant
Dedicated vector database adapter implementing EmbeddingAdapter only (no threads or entries). Use it alongside a conversation adapter when you need vector search at scale.
import { QdrantAdapter, unwrap } from '@cogitator-ai/memory';
const qdrant = new QdrantAdapter({
provider: 'qdrant',
url: 'http://localhost:6333',
collection: 'cogitator',
dimensions: 1536,
});
unwrap(await qdrant.connect());
await qdrant.addEmbedding({
sourceId: 'doc-42',
sourceType: 'document',
vector: embedding,
content: 'Agent memory architecture overview.',
});
const results = unwrap(
await qdrant.search({
vector: queryVector,
limit: 10,
threshold: 0.75,
filter: { sourceType: 'document' },
})
);| Option | Type | Default | Description |
|---|---|---|---|
url | string | http://localhost:6333 | Qdrant server URL |
apiKey | string | — | API key for Qdrant Cloud |
collection | string | cogitator | Collection name |
dimensions | number | — | Vector dimensions (must match model) |
connect() creates the collection with cosine distance if it does not exist. Search results carry the payload but not the stored vector (vector is []), and no threshold is applied unless you pass one. Embedding metadata is stored in the point payload, so filter.threadId / filter.agentId match the metadata.threadId / metadata.agentId payload keys, and filter.userId matches metadata.userId or points without a user.
Qdrant only accepts unsigned integers and UUIDs as point ids, so each point is stored under a deterministic UUID derived from the embedding id. The emb_… id itself is kept in payload.embeddingId; it is what addEmbedding and search return and what deleteEmbedding takes.
Requires: pnpm add @qdrant/js-client-rest
When to use: Large-scale semantic search, production RAG pipelines, or when you want a dedicated vector DB separate from your relational store.
InMemoryEmbedding
An in-memory adapter that implements both EmbeddingAdapter and KeywordSearchAdapter using cosine similarity and a built-in BM25 index. Useful for testing hybrid search without external services.
import { InMemoryEmbeddingAdapter, unwrap } from '@cogitator-ai/memory';
const embeddings = new InMemoryEmbeddingAdapter();
await embeddings.addEmbedding({
sourceId: 'doc-1',
sourceType: 'document',
vector: [0.1, 0.2, 0.3],
content: 'Sample document content',
});
const vectorResults = unwrap(
await embeddings.search({
vector: [0.1, 0.2, 0.3],
limit: 5,
})
);
const keywordResults = unwrap(
await embeddings.keywordSearch({
query: 'sample document',
limit: 5,
})
);
console.log(embeddings.size); // 1
embeddings.clear();It needs no connect(). Vector search uses a default threshold of 0.7, and both searches support the same filter as Postgres (sourceType, agentId, threadId, userId).
Comparison
| Feature | InMemory | Redis | Postgres | SQLite | MongoDB | Qdrant |
|---|---|---|---|---|---|---|
| Threads/Entries | Yes | Yes | Yes | Yes | Yes | No |
| Facts | No | No | Yes | No | No | No |
| Vector Search | No¹ | No | pgvector | No | No | Yes |
| Keyword Search | No¹ | No | tsvector | No | No | No |
| Knowledge Graph | No | No | Yes² | Yes² | No | No |
| TTL Expiration | No | Yes | No | No | No | No |
memory.adapter³ | Yes | Yes | Yes | Yes | Yes | memory.qdrant |
| Extra Dependencies | None | ioredis | pg | better-sqlite3 | mongodb | @qdrant/js-client-rest |
¹ Use InMemoryEmbeddingAdapter. ² Via PostgresGraphAdapter / SQLiteGraphAdapter — see Knowledge Graphs. ³ Can be selected as the thread store in the Cogitator memory config. Qdrant is configured next to a thread store as memory.qdrant, the embedding store semantic context searches. Any adapter can also be assigned with cog.memory = adapter — see Using Memory with Cogitator.