# Memory Adapters

URL: https://cogitator.app/docs/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:

```typescript
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()`](/docs/memory#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()`.

```typescript
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.

```typescript
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:

```typescript
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.

```typescript
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)

```typescript
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)

```typescript
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](/docs/memory/embeddings) 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`:

```typescript
const hits = unwrap(
  await memory.keywordSearch({
    query: 'LLM backends',
    limit: 10,
    filter: { sourceType: 'document' },
  })
);
```

**Requires:** `pnpm add pg` and the [pgvector](https://github.com/pgvector/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.

```typescript
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):

```typescript
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`](/docs/memory/knowledge-graphs#graph-adapters).

**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)`.

```typescript
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.

```typescript
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.

```typescript
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](/docs/memory/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](/docs/memory#using-memory-with-cogitator).
