engram
README.md
# engram
A learning agent memory system with reflect loop. Engram stores facts extracted from text, links them to entities and relationships, and periodically synthesizes higher-order observations (patterns, preferences, insights).
Engram is two things:
1. **A TypeScript library** — import `Engram` and call `retain()`, `recall()`, `reflect()` directly.
2. **An MCP server** — run `engram-mcp` and connect any MCP client (Claude Desktop, Claude Code, etc.) over stdio.
Both use the same SQLite-backed engine. No external database required.
## 60-Second Quickstart
The fastest way to get started with engram as an MCP server in Claude Code:
```bash
npx engram-init
```
This interactive setup wizard will:
1. Detect your environment (OpenAI/Anthropic keys, Ollama, Claude Code CLI)
2. Let you choose your AI provider (OpenAI, Anthropic, or fully-local Ollama)
3. Test the connection
4. Register engram with Claude Code automatically
Then start a new Claude Code session and try:
- *"Remember that I prefer TypeScript with strict mode"*
- *"What do you remember about my preferences?"*
**Manual MCP setup** (if you prefer): See the [MCP Server](#mcp-server) section below.
## How It Works
```
text
|
v
+---------+---------+
| retain() | Extract facts, entities, relations via LLM
| (or retainDirect)| Store with embeddings + FTS index
+---------+---------+
|
v
+---------+---------+
| recall() | Hybrid search: semantic + keyword + temporal
| | Rank fusion with configurable weights
+---------+---------+
|
v
+---------+---------+
| reflect() | Cluster facts by shared entities
| | Synthesize observations (patterns/preferences/insights)
+---------+---------+
```
**retain** — Ingests text, extracts atomic facts and entities via an LLM, embeds them, and stores everything in SQLite. Duplicate facts are skipped via content-hash deduplication. Optionally evaluates new facts against existing observations to adjust confidence.
**retainDirect** — Same as retain but accepts pre-extracted facts/entities/relations, skipping the LLM extraction call entirely. Useful when you already have structured data.
**recall** — Hybrid retrieval combining semantic similarity (sqlite-vec), keyword matching (FTS5), and temporal filtering (chrono-node). Scores are fused with configurable weights and observations get a priority boost.
**reflect** — Groups facts by shared entities into clusters, then synthesizes higher-order observations using an LLM. Duplicate observations merge evidence and bump confidence. Low-confidence observations are archived.
**forget** — Removes facts by ID, age, or TTL expiry. FTS5 and entity links are cleaned up automatically.
## Quick Start
### Programmatic (TypeScript library)
```typescript
import { Engram, createProviders } from 'engram';
const { embedding, llm } = await createProviders({ provider: 'openai' });
const engram = new Engram(
{ dbPath: './memory.sqlite' },
embedding,
llm,
);
// Store information (LLM extracts facts automatically)
await engram.retain({
text: 'Alice prefers TypeScript over JavaScript. She works at Acme Corp.',
source: 'conversation',
});
// Store pre-extracted facts (no LLM call)
await engram.retainDirect({
facts: [{ content: 'Bob uses vim keybindings' }],
entities: [{ name: 'Bob', entityType: 'person' }],
});
// Search memory
const results = await engram.recall({
query: 'What does Alice prefer?',
limit: 5,
});
// Synthesize observations
await engram.reflect({ force: true });
// Remove a fact
engram.forget({ factId: 'some-fact-id' });
// Remove old facts
engram.forget({ olderThan: 30 * 24 * 60 * 60 * 1000 }); // 30 days
// List all memory banks
const banks = engram.listBanks();
engram.close();
```
### MCP Server
```bash
# Set your API key
export OPENAI_API_KEY=sk-...
# Start the MCP server
npx engram-mcp
```
Add to your MCP client configuration (e.g. Claude Desktop):
```json
{
"mcpServers": {
"engram": {
"command": "npx",
"args": ["engram-mcp"],
"env": {
"OPENAI_API_KEY": "sk-..."
}
}
}
}
```
## MCP Tools Reference
| Tool | Description | Key Parameters |
|------|-------------|---------------|
| `retain` | Store text, extracting facts via LLM | `text`, `source?`, `bank?`, `max_age_ms?` |
| `retain_direct` | Store pre-extracted facts (no LLM) | `facts[]`, `entities?[]`, `relations?[]`, `bank?`, `max_age_ms?` |
| `recall` | Search memory with hybrid retrieval | `query`, `limit?`, `bank?`, `include_observations?`, `time_filter?` |
| `reflect` | Synthesize observations from facts | `focus?`, `bank?` |
| `inspect` | View stats, look up entities, or list banks | `bank?`, `entity?`, `list_banks?` |
| `forget` | Remove facts from memory | `fact_id?`, `before?`, `older_than_days?`, `bank?` |
## Concepts
### Banks
Memory banks provide isolated namespaces. Every fact, entity, and observation is scoped to a bank. Use banks to separate contexts (e.g. "work" vs "personal", or per-user).
```typescript
engram.createBank('work', 'Work', 'Professional context');
await engram.retain({ text: '...', bankId: 'work' });
const banks = engram.listBanks(); // discover all banks
```
### Facts vs Observations
- **Facts** are atomic pieces of information extracted from input text. Each has a content hash for deduplication, a confidence score, and optional entity links.
- **Observations** are higher-order insights synthesized by the reflect loop. Types: `pattern`, `preference`, `insight`. They track confidence that adjusts as new evidence arrives.
### Confidence
- Facts start at confidence 1.0.
- Observations start at 0.5 and adjust based on supporting/contradicting evidence.
- Observations below 0.1 confidence are archived during reflect.
### Entity Resolution
Entities are deduplicated by `(name, type, bank)`. When the same entity name appears again, metadata is merged rather than creating a duplicate.
## Configuration
```typescript
interface EngramConfig {
dbPath: string; // SQLite database path (default: ':memory:')
defaultBankId?: string; // Default bank ID (default: 'default')
defaultBankName?: string; // Default bank name (default: 'Default')
reflectThreshold?: number; // Facts before auto-reflect triggers (default: 20)
reflectInterval?: number; // Min ms between reflects (default: 86400000 / 24h)
autoReflect?: boolean; // Enable auto-reflect after retain (default: true)
skipEvidenceEvaluation?: boolean; // Skip LLM evidence eval on retain (default: false)
reflectMaxClusters?: number; // Max clusters to process per reflect (default: 0 = unlimited)
}
```
## Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `ENGRAM_DB_PATH` | SQLite database file path | `~/.engram/memory.sqlite` |
| `ENGRAM_PROVIDER` | AI provider: `openai`, `anthropic`, `ollama` | `openai` |
| `ENGRAM_EMBEDDING_MODEL` | Override embedding model name | Provider default |
| `ENGRAM_LANGUAGE_MODEL` | Override language model name | Provider default |
| `ENGRAM_DEFAULT_BANK` | Default bank ID | `default` |
| `ENGRAM_DEFAULT_BANK_NAME` | Default bank name | `Default` |
| `ENGRAM_AUTO_REFLECT` | Set to `false` to disable auto-reflect | `true` |
| `ENGRAM_SKIP_EVIDENCE_EVAL` | Set to `true` to skip evidence evaluation | `false` |
| `ENGRAM_REFLECT_MAX_CLUSTERS` | Max clusters per reflect cycle | unlimited |
## Provider Support
### OpenAI (default)
```bash
export OPENAI_API_KEY=sk-...
export ENGRAM_PROVIDER=openai
```
- Embedding: `text-embedding-3-small` (1536 dimensions)
- Language: `gpt-4o-mini`
### Anthropic
```bash
export OPENAI_API_KEY=sk-... # Still needed for embeddings
export ANTHROPIC_API_KEY=sk-ant-...
export ENGRAM_PROVIDER=anthropic
```
- Embedding: OpenAI `text-embedding-3-small` (1536 dimensions)
- Language: `claude-sonnet-4-5-20250929`
### Ollama (fully local, no API key)
Run everything locally with no API keys or cloud calls:
```bash
ollama pull nomic-embed-text
ollama pull llama3.2
ENGRAM_PROVIDER=ollama npm run mcp
```
- Embedding: `nomic-embed-text` (768 dimensions)
- Language: `llama3.2`
### Custom Providers
Implement the `EmbeddingProvider` and `LLMProvider` interfaces:
```typescript
import type { EmbeddingProvider, LLMProvider } from 'engram';
class MyEmbedding implements EmbeddingProvider {
readonly dimensions = 1536;
async embed(text: string): Promise<Float32Array> { /* ... */ }
async embedMany(texts: string[]): Promise<Float32Array[]> { /* ... */ }
}
class MyLLM implements LLMProvider {
async extractFacts(text: string, bankContext?: string) { /* ... */ }
async synthesizeObservations(facts, existing, bankContext?) { /* ... */ }
async evaluateEvidence(fact, observation) { /* ... */ }
}
const engram = new Engram(config, new MyEmbedding(), new MyLLM());
```
## Cost Controls
The reflect loop and evidence evaluation make LLM calls that can add up. Two config options help control costs:
**`skipEvidenceEvaluation: true`** — Skips the fire-and-forget LLM call that evaluates each new fact against every active observation. This removes `O(facts * observations)` LLM calls per retain. Observation confidence will only change during reflect.
**`reflectMaxClusters: N`** — Limits reflect to the N largest entity clusters (by fact count). This caps the number of synthesis LLM calls per reflect cycle.
```typescript
const engram = new Engram({
dbPath: './memory.sqlite',
skipEvidenceEvaluation: true, // No evidence eval LLM calls
reflectMaxClusters: 5, // Max 5 clusters per reflect
}, embedding, llm);
```
## Multi-Context via Banks
Banks are isolated memory namespaces. Use them for multi-user, multi-project, or multi-domain scenarios:
```typescript
// Create isolated banks
engram.createBank('user-alice', 'Alice', 'Alice personal memory');
engram.createBank('user-bob', 'Bob', 'Bob personal memory');
// Store to specific banks
await engram.retain({ text: '...', bankId: 'user-alice' });
// Search within a bank
await engram.recall({ query: '...', bankId: 'user-alice' });
// Discover all banks
const banks = engram.listBanks();
```
## TTL and Forgetting
Facts can be given a time-to-live (TTL) so they expire automatically:
```typescript
// Facts expire after 1 hour
await engram.retain({ text: '...', maxAge: 3600000 });
// Pre-extracted facts with TTL
await engram.retainDirect({ facts: [...], maxAge: 86400000 }); // 24h
// Manually prune expired facts
engram.pruneExpired();
// Forget specific fact
engram.forget({ factId: 'abc-123' });
// Forget facts older than 30 days
engram.forget({ olderThan: 30 * 24 * 60 * 60 * 1000 });
// Forget facts before a date
engram.forget({ before: new Date('2024-01-01') });
```
Expired facts are also opportunistically pruned at the start of each `retain()` and `retainDirect()` call.
## Testing and Development
**Requirements:** Node >= 20
### Project Structure
```
src/
engram.ts Main orchestrator (Engram class)
types.ts All domain interfaces
index.ts Public API exports
store/
schema.ts SQLite DDL (tables, FTS5, triggers, vec0)
migrations.ts Forward-only schema migrations
store.ts MemoryStore — SQLite persistence layer
pipelines/
retain.ts Ingestion: extract → embed → store
recall.ts Retrieval: hybrid search + rank fusion
reflect.ts Synthesis: cluster → synthesize → archive
extract.ts LLM prompt builders
providers/
types.ts EmbeddingProvider + LLMProvider interfaces
ai-sdk.ts Vercel AI SDK implementation
defaults.ts Provider factory (openai/anthropic/ollama)
mcp/
server.ts MCP server (6 tools + 1 resource)
utils/
hash.ts SHA-256 content hashing
temporal.ts Natural language date parsing
entity-resolver.ts Entity dedup/merge
rank-fusion.ts Weighted score fusion
bin/
engram-mcp.ts CLI entry point for MCP server
tests/
helpers/
mock-providers.ts Deterministic 4D mock embedding + mock LLM
unit/ Store, hash, temporal, rank-fusion, entity-resolver
integration/ Full retain → recall → reflect cycles
```
### Running Tests
Tests use **mock providers** — deterministic 4-dimensional embeddings (MD5-based) and a mock LLM that does simple sentence splitting. No API key or network calls needed.
```bash
# Run all tests (57 tests, ~1s)
npm test
# Watch mode
npm run test:watch
```
### Building
```bash
npm run build # TypeScript → dist/
npm run dev # TypeScript watch mode
```
### Testing the MCP Server Locally
**Option 1: With the MCP Inspector** (interactive web UI)
```bash
npm run build
npx @modelcontextprotocol/inspector node dist/bin/engram-mcp.js
```
This opens a browser UI where you can call `retain`, `recall`, `reflect`, `forget`, etc. interactively.
**Option 2: With a real provider**
```bash
export OPENAI_API_KEY=sk-...
npm run mcp
```
This starts the stdio MCP server. Connect any MCP client to it.
**Option 3: Fully local with Ollama** (no API key)
```bash
ollama pull nomic-embed-text && ollama pull llama3.2
ENGRAM_PROVIDER=ollama npm run mcp
```
### Writing Tests
Tests follow existing patterns in `tests/`. Use the mock providers from `tests/helpers/mock-providers.ts`:
```typescript
import { Engram } from '../../src/engram.js';
import { MockEmbeddingProvider, MockLLMProvider } from '../helpers/mock-providers.js';
const embedding = new MockEmbeddingProvider(); // 4D deterministic vectors
const llm = new MockLLMProvider(); // Sentence splitting, no network
const engram = new Engram({ dbPath: ':memory:' }, embedding, llm);
```
The `MockLLMProvider` accepts a custom `extractFn` if you need to control extraction behavior for a specific test.
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessUnresponsive