Cogitator
Memory & RAG

Memory Overview

Understand how Cogitator agents persist context across conversations using pluggable memory adapters, embeddings, and knowledge graphs.

Why Memory Matters

LLMs are stateless. Every request starts from scratch unless you explicitly pass conversation history. As conversations grow, they exceed context window limits. Memory solves this by persisting, indexing, and intelligently retrieving past interactions.

Cogitator's memory system gives agents:

  • Conversation persistence across sessions via threads and entries
  • Semantic search over stored documents and messages using vector embeddings
  • Fact storage for long-term knowledge (user preferences, learned info)
  • Knowledge graphs for structured entity and relationship tracking
  • Token-aware context building that fits the most relevant history into the model's context window

Architecture

┌────────────────────────────────────────────────────────────────┐
│                         ContextBuilder                          │
│   Assembles messages, facts, semantic and graph context         │
│   within a token budget                                         │
├───────────────┬──────────────┬──────────────────┬──────────────┤
│ MemoryAdapter │ FactAdapter  │ EmbeddingAdapter │ GraphAdapter │
│ (threads +    │ (long-term   │ (vector search)  │ (entities +  │
│  entries)     │  facts)      │                  │  relations)  │
├───────────────┴──────────────┴──────────────────┴──────────────┤
│  InMemory │ Redis │ Postgres │ SQLite │ MongoDB │ Qdrant        │
└────────────────────────────────────────────────────────────────┘

Every conversation adapter implements the same MemoryAdapter interface, so you can swap backends without changing application code. Facts, embeddings and graphs are separate interfaces that only some backends implement — see Choosing an Adapter.

Results and unwrap()

Adapter methods never throw for expected failures; they return a MemoryResult:

type MemoryResult<T> = { success: true; data: T } | { success: false; error: string };

Check result.success, or use unwrap() to get the data and throw an Error with the adapter's message on failure:

import { InMemoryAdapter, unwrap } from '@cogitator-ai/memory';

const memory = new InMemoryAdapter();
await memory.connect();

const thread = unwrap(await memory.createThread('agent-1'));

const result = await memory.getThread(thread.id);
if (result.success && result.data) {
  console.log(result.data.metadata);
}

Core Concepts

Threads

A Thread represents a conversation session. Each thread belongs to an agent and holds an ordered list of memory entries. Pass a third argument to choose the thread id yourself:

import { InMemoryAdapter, unwrap } from '@cogitator-ai/memory';

const memory = new InMemoryAdapter();
await memory.connect();

const thread = unwrap(await memory.createThread('agent-1', { topic: 'onboarding' }));
const named = unwrap(await memory.createThread('agent-1', {}, 'support-ticket-42'));

metadata.userId names the user a thread belongs to. When an agent run creates a thread, the runtime sets it from the run's userId — see Multiple Users.

Memory Entries

A MemoryEntry stores a single message along with its token count and optional tool call data.

await memory.addEntry({
  threadId: thread.id,
  message: { role: 'user', content: 'Hello!' },
  tokenCount: 3,
});

await memory.addEntry({
  threadId: thread.id,
  message: { role: 'assistant', content: 'Hi there! How can I help?' },
  tokenCount: 8,
});

const entries = unwrap(
  await memory.getEntries({
    threadId: thread.id,
    limit: 50,
    includeToolCalls: true,
  })
);

getEntries() returns entries oldest first; limit keeps the most recent ones, and before / after (dates) narrow the range. Tool calls and results are stripped unless includeToolCalls is true.

Facts

Facts are long-term memories that persist across threads: user preferences, learned information, or system-level knowledge. Only PostgresAdapter implements FactAdapter.

import { PostgresAdapter } from '@cogitator-ai/memory';

const postgres = new PostgresAdapter({
  provider: 'postgres',
  connectionString: process.env.DATABASE_URL!,
});
await postgres.connect();

await postgres.addFact({
  agentId: 'agent-1',
  content: 'User prefers dark mode',
  category: 'preference',
  confidence: 0.95,
  source: 'user',
  metadata: { userId: 'alice' },
});

A fact with metadata.userId only goes into that user's context; a fact without one is shared by everyone the agent serves. Facts with an expiresAt in the past are no longer returned.

Context Building

The ContextBuilder assembles the final message list sent to the LLM. It respects token limits by selecting the most relevant entries from memory.

import { ContextBuilder, createEmbeddingService } from '@cogitator-ai/memory';

const embeddings = createEmbeddingService({
  provider: 'openai',
  apiKey: process.env.OPENAI_API_KEY!,
});

const builder = new ContextBuilder(
  {
    maxTokens: 8192,
    strategy: 'hybrid',
    includeSystemPrompt: true,
    includeFacts: true,
    includeSemanticContext: true,
  },
  {
    memoryAdapter: postgres,
    factAdapter: postgres,
    embeddingAdapter: postgres,
    embeddingService: embeddings,
  }
);

const context = await builder.build({
  threadId: thread.id,
  agentId: 'agent-1',
  userId: 'alice',
  systemPrompt: 'You are a helpful assistant.',
  currentInput: 'What were we discussing yesterday?',
});

console.log(context.messages, context.tokenCount, context.truncated);
OptionDefaultDescription
maxTokens—Total token budget
reserveTokens10% of maxTokens (min 100)Tokens kept free for the model's reply
strategy—recent, relevant or hybrid
includeSystemPrompttruePut systemPrompt first
includeFactsfalseAdd a Known facts: block (needs factAdapter, up to 10% of the budget)
includeSemanticContextfalseAdd a Relevant context: block from the top 5 embeddings scoring ≥ 0.7 (needs embeddingAdapter + embeddingService and a currentInput, up to 10% of the budget)
includeGraphContextfalseAdd knowledge graph context (needs graphContextBuilder and a currentInput, up to 15% of the budget) — see Knowledge Graphs
graphContextOptions{}maxNodes, maxEdges, maxDepth, includeInferred, entityTypes

Facts, semantic and graph context are merged into the system message. The remaining budget is filled with conversation entries by the strategy:

StrategyBehavior
recentFills context with the newest entries that fit, as one unbroken run: it stops at the first entry that does not fit
relevantSelects entries by semantic similarity to the current input
hybridSpends up to 30% of the budget on older entries (outside the last 10) scoring above 0.6 similarity, the rest on recent entries

relevant and hybrid need an embeddingService and a currentInput; without them (or, for hybrid, with 10 entries or fewer) they fall back to recent. Selected messages are always returned in chronological order.

Semantic context is scoped per agent: embeddings whose metadata.agentId belongs to another agent are never injected, while embeddings without an agentId (shared documents) are visible to every agent.

Pass userId to build() to scope it per user as well: facts, embeddings and knowledge graph nodes whose metadata.userId belongs to another user are left out, while those without one are shared. Agent runs do this with the run's userId — see Multiple Users.

Using Memory with Cogitator

Configure memory on the runtime and every run with a threadId loads and saves its history automatically:

import { Cogitator, Agent } from '@cogitator-ai/core';

const cog = new Cogitator({
  memory: {
    adapter: 'postgres',
    postgres: { connectionString: process.env.DATABASE_URL! },
    embedding: { provider: 'openai', apiKey: process.env.OPENAI_API_KEY! },
    contextBuilder: {
      maxTokens: 8000,
      strategy: 'hybrid',
      includeFacts: true,
      includeSemanticContext: true,
    },
  },
});

const agent = new Agent({
  name: 'assistant',
  model: 'openai/gpt-5.5',
  instructions: 'You are a helpful assistant.',
});

await cog.run(agent, { input: 'Hi, I am Alice', threadId: 'thread-alice' });
await cog.run(agent, { input: 'What is my name?', threadId: 'thread-alice' });
  • memory.adapter names the thread store and takes its settings from the key of the same name: 'memory' (inMemory), 'redis' (redis.url, redis.host + redis.port, or redis.cluster), 'postgres' (postgres.connectionString), 'sqlite' (sqlite.path) or 'mongodb' (mongodb.uri). When the required setting is missing or the connection fails, the run goes on without memory and logs a warning. An adapter you connect yourself can be assigned with cog.memory = adapter; such runs load the last 20 entries, since the context builder is only created from the memory config.
  • With contextBuilder set, each run builds its context as described above (maxTokens defaults to 4000, strategy to 'recent'). A Postgres store is also the fact adapter, and the embedding adapter unless memory.qdrant is set. The runtime creates the Postgres embeddings table with the vector size of memory.embedding (768 without one). Without contextBuilder, a run loads the last 20 entries of the thread.
  • 'qdrant' is not a thread store. For semantic context from Qdrant, keep a thread store as adapter and add memory.qdrant (url, apiKey, collection, dimensions matching the embedding model) together with memory.embedding and contextBuilder: { includeSemanticContext: true }. The runtime searches the collection but does not write to it; add embeddings with your own QdrantAdapter. See Configuration for a YAML example.
  • Per run, useMemory: false disables memory, loadHistory: false / saveHistory: false skip loading or saving, and onMemoryError is called when a load or save fails.
  • await cog.getMemory() connects and returns the adapter before any run — see Cogitator.

Choosing an Adapter

AdapterPersistenceEmbeddingsFactsGraphBest For
InMemoryNoneSeparate¹NoNoTesting, short-lived agents
RedisTTL-basedNoNoNoShort-term memory with auto-expiry
PostgresPermanentpgvectorYesPostgresGraphAdapterProduction with full features
SQLitePermanentNoNoSQLiteGraphAdapterSingle-process, local apps
MongoDBPermanentNoNoNoDocument-oriented workloads
QdrantPermanentNativeNoNoDedicated vector search

¹ InMemoryEmbeddingAdapter provides in-process vector and keyword search.

For most production deployments, Postgres is the recommended adapter since it supports threads, facts, embeddings, keyword search and (with PostgresGraphAdapter) knowledge graphs in a single database. See Memory Adapters for configuration.

Sessions, Compaction and Core Facts

Three helpers build on top of any MemoryAdapter. The channels gateway wires them up for you; you can also use them directly.

SessionManager maps a user on a channel to a long-lived thread (session_<channelType>_<userId>) and tracks its status, message count and per-session config overrides:

import { createLLMBackend } from '@cogitator-ai/core';
import { SessionManager, CompactionService, InMemoryAdapter } from '@cogitator-ai/memory';

const memory = new InMemoryAdapter();
await memory.connect();

const llm = createLLMBackend('google', {
  providers: { google: { apiKey: process.env.GOOGLE_API_KEY! } },
});

const compaction = new CompactionService({
  adapter: memory,
  summarize: async (messages, { model, prompt } = {}) => {
    const response = await llm.chat({
      model: model ?? 'gemini-3.5-flash-lite',
      messages: [
        { role: 'system', content: prompt ?? 'Summarize this conversation in a few sentences.' },
        ...messages,
      ],
    });
    return response.content;
  },
});

const sessions = new SessionManager(memory, { compaction });

const session = await sessions.getOrCreate({
  userId: 'alice',
  channelType: 'telegram',
  channelId: 'chat-1',
  agentId: 'assistant',
});

await sessions.update(session.id, { config: { temperature: 0.2 } });
const active = await sessions.list({ status: 'active', limit: 20 });
await sessions.archive(session.id);

CompactionService shrinks a thread once its stored tokens reach threshold, keeping the keepRecent latest entries:

const result = await compaction.compact(session.id, {
  strategy: 'summary',
  threshold: 8000,
  keepRecent: 10,
  summaryModel: 'gemini-3.5-flash',
  summaryPrompt: 'Keep names, decisions and open questions.',
});
// { sessionId, originalMessages, compactedMessages, summaryTokens }
StrategyBehavior
summaryReplaces older entries with one [Conversation summary] system entry produced by your summarize
sliding-windowDeletes older entries
hybridsliding-window when 5 or fewer entries would be removed, otherwise summary

summarize is your function — call any LLM in it. It receives the messages being replaced and, as a second argument, SummarizeOptions { model, prompt } taken from the summaryModel / summaryPrompt of the compact() config (either may be undefined). sessions.compact() throws unless the manager was created with { compaction }.

CoreFactsStore is a small SQLite key–value store (requires better-sqlite3) for always-on facts about the user, with a history of every value:

import { CoreFactsStore } from '@cogitator-ai/memory';

const coreFacts = new CoreFactsStore({ path: './memory.db' });
await coreFacts.initialize();

await coreFacts.set('name', 'Alice');
await coreFacts.set('timezone', 'Europe/Berlin');

const prompt = await coreFacts.formatForPrompt(); // "name: Alice\ntimezone: Europe/Berlin"
const history = await coreFacts.getHistory('timezone'); // [{ value, setAt }]
await coreFacts.close();

Installation

pnpm add @cogitator-ai/memory

Optional peer dependencies based on your adapter choice:

pnpm add ioredis                  # Redis
pnpm add pg                       # Postgres (+ pgvector extension for embeddings)
pnpm add better-sqlite3           # SQLite, SQLiteGraphAdapter, CoreFactsStore
pnpm add mongodb                  # MongoDB
pnpm add @qdrant/js-client-rest   # Qdrant

Redis connections go through @cogitator-ai/redis, which is installed with the memory package and uses ioredis under the hood.

On this page