Cogitator
Memory & RAG

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 }
OptionTypeDefaultDescription
maxEntriesnumber10000Max 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,
});
OptionTypeDefaultDescription
urlstring—Redis URL for standalone mode
hoststring—Host (alternative to url)
portnumber—Port (alternative to url)
clusterobject—nodes and scaleReads (master/slave/all)
keyPrefixstringcogitator: or {cogitator}: for clusterKey prefix for all Redis keys
ttlnumber86400TTL in seconds (24h default)
passwordstring—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.

OptionTypeDefaultDescription
connectionStringstring—Postgres connection URL
schemastringcogitatorSchema name (letters, digits and underscores only)
poolSizenumber10Connection 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:' });
OptionTypeDefaultDescription
pathstring—Path to .db file, or :memory:
walModebooleantrueEnable 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());
OptionTypeDefaultDescription
uristring—MongoDB connection URI
databasestringcogitatorDatabase name
collectionPrefixstringmemory_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' },
  })
);
OptionTypeDefaultDescription
urlstringhttp://localhost:6333Qdrant server URL
apiKeystring—API key for Qdrant Cloud
collectionstringcogitatorCollection name
dimensionsnumber—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

FeatureInMemoryRedisPostgresSQLiteMongoDBQdrant
Threads/EntriesYesYesYesYesYesNo
FactsNoNoYesNoNoNo
Vector SearchNo¹NopgvectorNoNoYes
Keyword SearchNo¹NotsvectorNoNoNo
Knowledge GraphNoNoYes²Yes²NoNo
TTL ExpirationNoYesNoNoNoNo
memory.adapter³YesYesYesYesYesmemory.qdrant
Extra DependenciesNoneioredispgbetter-sqlite3mongodb@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.

On this page