@memstack/core
v0.6.4
Published
Open-source AI memory framework — store, retrieve, summarize, and prune memories for LLM agents and games
Maintainers
Readme
MemStack
The open-source memory layer for AI agents — store, retrieve, summarize, and prune.
npm install @memstack/coreThe problem: AI agents forget. Every interaction starts from zero. You either stuff everything into the context window (expensive, slow, degrades output quality) or the agent has no memory of past conversations.
What MemStack does: A persistent memory pipeline that lives between your agent and the LLM. It stores every interaction, retrieves only what's relevant, summarizes old memories to save tokens, and prunes stale ones automatically. One method call, no infrastructure required.
Think of it as the open-source alternative to Mem0 — pluggable storage, bring your own LLM, zero vendor lock-in.
Table of Contents
- Why MemStack
- Quick Start
- The Memory Pipeline
- Real-World Use Cases
- Memory Type Reference
- Retrieval Strategies
- Embeddings
- Adapters
- Full API Reference
- Configuration
- Advanced Usage
- Development
- Publishing to npm
- Contributing
- License
Why MemStack
LLMs have context windows, not memory. The difference matters.
| Approach | Problem | |----------|---------| | Stuff everything in context | Cost is O(n²). 100 conversations = thousands of tokens = dollars per call. Quality degrades from "lost in the middle" effect. | | Use a vector DB directly | You get similarity search. You don't get summarization, pruning, recency weighting, deduplication, or token budget management. You're building the pipeline yourself. | | Use Mem0 | Proprietary, cloud-only with their hosted API. You don't control where your data lives. | | Use MemStack | Full pipeline. Pluggable everything. Your data, your infrastructure. Open source. |
What MemStack handles that raw vector DBs don't:
- Summarization — compress 100 old interactions into one paragraph, keep meaning, save tokens
- Recency weighting — recent memories matter more; MemStack sorts them higher
- Importance scoring — not all memories are equal; high-importance ones survive pruning
- Deduplication — identical or near-identical memories are collapsed in context assembly
- Token budget —
compileContext()tells you how many tokens you're spending before the LLM call - Memory-type routing — interactions, summaries, observations treated differently at retrieval time
- Auto-pruning — old, low-importance memories clean themselves up
Quick Start
npm install @memstack/coreOpenAI
import { MemStack, OpenAILLMAdapter, OpenAIEmbeddingAdapter, InMemoryStorageAdapter } from "@memstack/core";
const llm = new OpenAILLMAdapter({ apiKey: process.env.OPENAI_API_KEY! });
const memstack = new MemStack({
llm,
embedding: new OpenAIEmbeddingAdapter({ apiKey: process.env.OPENAI_API_KEY! }),
storage: new InMemoryStorageAdapter(),
});DeepSeek (no embeddings)
DeepSeek provides chat completions but has no embedding API. Use the OpenAI-compatible LLM adapter with baseURL and omit the embedding adapter — retrieval falls back to keyword + recency + importance ranking. You still get the full pipeline: store, summarize, prune, and compileContext.
import { MemStack, OpenAILLMAdapter, InMemoryStorageAdapter } from "@memstack/core";
const llm = new OpenAILLMAdapter({
apiKey: process.env.DEEPSEEK_API_KEY!,
baseURL: "https://api.deepseek.com/v1",
defaultModel: "deepseek-chat",
});
const memstack = new MemStack({
llm,
storage: new InMemoryStorageAdapter(),
// No embedding adapter — retrieval uses keyword matching
});OpenRouter / Together AI / any OpenAI-compatible API
Same pattern — change baseURL and defaultModel:
// OpenRouter
const llm = new OpenAILLMAdapter({
apiKey: process.env.OPENROUTER_API_KEY!,
baseURL: "https://openrouter.ai/api/v1",
defaultModel: "openai/gpt-4o-mini",
});
// Together AI
const llm = new OpenAILLMAdapter({
apiKey: process.env.TOGETHER_API_KEY!,
baseURL: "https://api.together.xyz/v1",
defaultModel: "meta-llama/Llama-3.3-70B-Instruct-Turbo",
});
// Gemini (OpenAI-compatible endpoint)
const llm = new OpenAILLMAdapter({
apiKey: process.env.GEMINI_API_KEY!,
baseURL: "https://generativelanguage.googleapis.com/v1beta/openai",
defaultModel: "gemini-2.0-flash",
});Store and retrieve
// 1. Store what happened
await memstack.memory.store({
actorId: "support-bot-42",
content: "User reports login failing with error 503 on Chrome 125.",
tags: ["login", "bug", "chrome"],
importance: 0.8,
});
// 2. Later, retrieve relevant context
const memories = await memstack.memory.retrieve({
actorId: "support-bot-42",
query: "login error",
strategy: "hybrid",
});
// 3. Assemble an LLM-ready context
const ctx = await memstack.memory.compileContext({
actorId: "support-bot-42",
maxTokens: 2000,
});
const response = await llm.complete({
system: `You are a support bot. Here is what you remember:\n${ctx.systemPrompt}`,
user: "The user is back and still can't log in. What do you do?",
});
console.log(response.text);
// "Based on our history, the user has been experiencing 503 errors on Chrome 125..."
// 4. Every 100 interactions, summarization triggers automatically.
// Old interactions are compressed into a paragraph. Token costs stay flat.The Memory Pipeline
MemStack's core is a five-stage pipeline. Each stage can be used independently.
1. Store
Every agent interaction becomes a Memory with metadata that controls how it's retrieved, summarized, and pruned later.
interface Memory {
id: string;
actorId: string; // Who this memory belongs to (user ID, agent ID, session ID)
memoryType: MemoryType; // "interaction" | "summary" | "observation" | "fact" | "reflection"
content: string; // The actual text
importance: number; // 0-1 — higher = survives pruning, ranks higher in retrieval
emotionalValence: number; // -1 to 1 — for tone-aware retrieval
tags: string[]; // Filter by tag: "bug", "billing", "urgent", etc.
embedding?: number[]; // Computed automatically if embedding adapter is configured
metadata?: Record<string, unknown>; // Your custom fields
expiresAt?: Date; // Auto-pruned after this date
sourceId?: string; // Link back to the originating event
createdAt: Date;
}// Simple store
await ms.memory.store({
actorId: "agent-7",
content: "Customer asked about refund policy for Q2 purchases.",
tags: ["billing", "refund"],
});
// Batch store — embeddings are batched into one API call for efficiency
await ms.memory.storeBatch([
{ actorId: "agent-7", content: "First interaction" },
{ actorId: "agent-7", content: "Second interaction" },
{ actorId: "agent-7", content: "Third interaction" },
]);2. Retrieve
Pull back what's relevant — by keyword, by meaning (semantic), by recency, or by importance.
const memories = await ms.memory.retrieve({
actorId: "agent-7", // Scope to one actor
query: "refund policy", // What to search for
strategy: "hybrid", // How to rank: "recent" | "important" | "semantic" | "hybrid"
limit: 10, // Max results
memoryTypes: ["interaction"], // Only certain types
tags: ["billing"], // Only certain tags
});Strategy behavior:
| Strategy | Sorts by | Requires embeddings | Best for |
|----------|----------|--------------------|----------|
| recent | Newest first | No | Knowing what just happened |
| important | Highest importance first | No | Filtering noise, keeping signal |
| semantic | Cosine similarity to query | Yes | "Find memories about X" |
| hybrid | Semantic + importance blend | Yes | Best of both worlds |
No embedding adapter? semantic and hybrid fall back to keyword matching + importance sort. No API costs, just less precise.
3. Compile Context
compileContext() takes retrieval results and assembles an LLM-ready system prompt — deduplicated, sorted by recency and importance, with a token estimate so you know the cost before calling the LLM.
const ctx = await ms.memory.compileContext({
actorId: "agent-7",
maxTokens: 2000, // Budget — assembler stops when it hits this
memoryTypes: ["interaction", "summary"],
});
// ctx.systemPrompt:
// ## Important Memories
// - The customer has been attempting login for 3 days. (importance: 0.85)
// - Refund was processed for order #4521 on Jan 12. (importance: 0.72)
//
// ## Recent Interactions
// - Customer asked about refund policy for Q2 purchases.
// - Customer reported login error 503 on Chrome 125.
console.log(ctx.tokenEstimate); // ~280
// Inject into your LLM call
const currentMessage = "The user is asking about their refund status.";
const response = await llm.complete({
system: ctx.systemPrompt,
user: currentMessage,
});
console.log(response.text);compileContext() handles deduplication, token budgeting, and splits context into important-vs-recent sections. Without it, you'd be concatenating raw retrieval results and risking context-window overflow.
4. Summarize
When an actor has hundreds of interactions, retrieval gets expensive and context gets bloated. Summarization compresses old interactions into a single paragraph using the configured LLM.
const { summary, deletedCount } = await ms.memory.summarize({
actorId: "agent-7",
olderThan: new Date(Date.now() - 7 * 86400000), // Older than 7 days
skipMostRecent: 10, // Never touch the 10 most recent
targetCount: 50, // Summarize at most 50 memories
memoryTypes: ["interaction"],
keepOriginals: false, // Delete originals after summary
});
// summary.content:
// "Over the past week, the customer reported recurring login failures (error 503)
// on Chrome 125. Multiple troubleshooting attempts including cache clearing and
// password reset were unsuccessful. A refund was processed for order #4521."
console.log(deletedCount); // 47 — 47 interactions compressed into 1 summary memoryAuto-summarization: Set summarizationThreshold in config (default: 100). Every 100th interaction for an actor triggers summarization automatically.
Warning: keepOriginals: false deletes the summarized memories. Set keepOriginals: true to preserve them alongside the summary.
Custom summarization prompt:
const ms = new MemStack({
llm,
defaults: {
summarizationPrompt:
"You are an enterprise support memory compressor. Highlight: customer name,
product, severity, resolution status, and any open issues.",
},
});5. Prune
Not all memories deserve to live forever. Pruning removes low-value memories to keep storage and retrieval fast.
// Remove memories older than 30 days
await ms.memory.prune({ type: "byAge", maxAge: 30 * 86400000 });
// Keep only memories above importance 0.3
await ms.memory.prune({ type: "byImportance", minImportance: 0.3 });
// Keep at most 500 memories per actor
await ms.memory.prune({ type: "byCount", maxPerActor: 500 });
// Remove specific types
await ms.memory.prune({ type: "byType", memoryTypes: ["observation"] });
// Custom logic
await ms.memory.prune({
type: "custom",
shouldRemove: (memory) => memory.content.includes("[RESOLVED]"),
});
// Dry run first — see what would be removed
const { wouldPrune, count } = await ms.memory.dryRunPrune({
type: "byAge",
maxAge: 86400000,
});
console.log(`Would remove ${count} memories:`, wouldPrune);Auto-prune on every process() call by setting pruneStrategy in config:
const ms = new MemStack({
llm,
defaults: {
pruneStrategy: { type: "byImportance", minImportance: 0.05 },
},
});Real-World Use Cases
Support Agent
// detectUrgency and classifyIntent are your own business logic.
// They could be simple keyword matchers, regex, or an LLM call.
function detectUrgency(msg: string): number {
if (msg.match(/urgent|asap|immediately/i)) return 0.9;
if (msg.match(/error|fail|broken/i)) return 0.7;
return 0.5;
}
function classifyIntent(msg: string): string[] {
const tags: string[] = [];
if (msg.match(/bill|refund|charge|payment/i)) tags.push("billing");
if (msg.match(/error|bug|fail|crash/i)) tags.push("bug");
if (msg.match(/login|password|account/i)) tags.push("account");
return tags;
}
// Every customer message becomes a memory
async function handleMessage(customerId: string, message: string) {
await ms.memory.store({
actorId: `customer:${customerId}`,
content: message,
importance: detectUrgency(message),
tags: classifyIntent(message),
});
// Retrieve everything relevant to this customer's history
const ctx = await ms.memory.compileContext({
actorId: `customer:${customerId}`,
maxTokens: 1500,
});
const response = await llm.complete({
system: `You are a support agent. Customer history:\n${ctx.systemPrompt}`,
user: message,
});
return response.text;
}
// Every 100th interaction, old history auto-compresses.
// A customer with 10,000 messages still fits in a $0.02 LLM call.RAG Pipeline
// Suppose you have documents from your knowledge base
const documents = [
{ text: "Authentication uses JWT tokens with 15-minute expiry.", url: "/docs/auth", section: "security" },
{ text: "Refunds are processed within 5-10 business days.", url: "/docs/billing", section: "billing" },
];
// Index documents as observation memories
for (const doc of documents) {
await ms.memory.store({
actorId: "knowledge-base",
content: doc.text,
memoryType: "observation",
metadata: { source: doc.url, section: doc.section },
});
}
// Query with semantic search
const relevantDocs = await ms.memory.retrieve({
actorId: "knowledge-base",
query: "How does authentication work?",
strategy: "semantic",
limit: 5,
});
const ctx = await ms.memory.compileContext({
actorId: "knowledge-base",
memoryTypes: ["observation"],
});
// Prompt the LLM with retrieved context
const answer = await llm.complete({
system: `Answer using only these documents:\n${ctx.systemPrompt}`,
user: "How does authentication work?",
});Multi-User Chatbot
// Each user gets their own memory space
async function chat(userId: string, message: string) {
await ms.memory.store({
actorId: userId,
content: message,
});
const ctx = await ms.memory.compileContext({
actorId: userId,
maxTokens: 1000,
});
return llm.complete({
system: `You are a friendly assistant. Conversation history with this user:\n${ctx.systemPrompt}`,
user: message,
});
}
// Get stats
const total = await ms.memory.count();
const userCount = await ms.memory.count({ actorId: "user-42" });Memory Type Reference
| Type | Purpose | Example |
|------|---------|---------|
| interaction | Default. Direct exchanges between agent and user/other agent. | "User asked about billing." |
| summary | Compressed collection of old interactions. Created by summarize(). | "Over 3 weeks, user reported 5 login failures..." |
| observation | Passive knowledge — facts, documents, things the agent knows but didn't interact with. | "Company refund policy is 30 days from purchase." |
| fact | Verified knowledge — discrete truths the agent has confirmed. | "The user's subscription tier is Enterprise." |
| reflection | Self-generated insight — the agent thinking about its own experiences. | "I tend to over-explain billing policies — should be more concise." |
Types control retrieval behavior — compileContext() treats interaction and summary differently from observation. Use types to separate "what happened" from "what I know."
Retrieval Strategies
Four strategies, each with a purpose:
// "What just happened?" — most recent first
await ms.memory.retrieve({ actorId: "x", strategy: "recent", limit: 3 });
// "What matters most?" — highest importance, ignoring age
await ms.memory.retrieve({ actorId: "x", strategy: "important" });
// "What relates to this query?" — cosine similarity search (needs embeddings)
await ms.memory.retrieve({ actorId: "x", query: "login bug", strategy: "semantic" });
// "Balance relevance and importance" — semantic + importance blend
await ms.memory.retrieve({ actorId: "x", query: "login bug", strategy: "hybrid" });Choosing a strategy:
- Use
recentfor chatbots, ongoing conversations, anything time-sensitive - Use
importantfor long-running agents where signal-to-noise matters - Use
semanticfor RAG, document search, knowledge base queries - Use
hybridfor most agent memory — it balances meaning with significance
Embeddings
Embeddings power semantic search. They're optional — without them, retrieval uses keyword matching.
With embeddings vs Without embeddings
With embeddings (embedding adapter configured):
import { MemStack, OpenAILLMAdapter, OpenAIEmbeddingAdapter, InMemoryStorageAdapter } from "@memstack/core";
const ms = new MemStack({
llm: new OpenAILLMAdapter({ apiKey: process.env.OPENAI_API_KEY! }),
embedding: new OpenAIEmbeddingAdapter({ apiKey: process.env.OPENAI_API_KEY! }),
storage: new InMemoryStorageAdapter(),
});
// store() computes a 1536-dim vector automatically
await ms.memory.store({
actorId: "agent-7",
content: "Customer asked about refund policy for Q2 purchases.",
});
// retrieve() with "semantic" or "hybrid" uses cosine similarity
// Query: "refund" finds the refund policy memory even though the word "refund"
// appears differently across stored memories.
const results = await ms.memory.retrieve({
actorId: "agent-7",
query: "how do I get my money back",
strategy: "semantic",
});
// Matches "Customer asked about refund policy" — semantic match, not keyword match.Without embeddings (no embedding adapter):
const ms = new MemStack({
llm: new OpenAILLMAdapter({ apiKey: process.env.OPENAI_API_KEY! }),
storage: new InMemoryStorageAdapter(),
// no embedding adapter
});
// store() works identically, just no vector computed
await ms.memory.store({
actorId: "agent-7",
content: "Customer asked about refund policy for Q2 purchases.",
});
// retrieve() with "semantic" or "hybrid" falls back to keyword matching
// plus importance/recency sorting. No API costs, no setup required.
const results = await ms.memory.retrieve({
actorId: "agent-7",
query: "refund",
strategy: "hybrid", // falls back to keyword + importance
});
// Still works — finds "refund" via substring match. Less precise for
// paraphrased queries ("money back" won't match "refund").Batch embedding: storeBatch() sends all texts in one embedding API call, reducing cost and latency.
// Disable auto-embedding if you only need keyword search
const ms = new MemStack({
llm,
embedding: new OpenAIEmbeddingAdapter({ apiKey }),
defaults: { embedOnStore: false },
});Vector dimensions and model compatibility
Different embedding models produce vectors of different lengths. Cosine similarity only works between vectors of the same dimension. If you change embedding models, existing vectors become incompatible — they can't be compared to new ones.
| Adapter | Default model | Dimensions |
|---------|--------------|------------|
| OpenAIEmbeddingAdapter | text-embedding-3-small | 1536 |
| OpenAIEmbeddingAdapter | text-embedding-3-large | 3072 |
| CohereEmbeddingAdapter | embed-english-v3.0 | 1024 |
| CohereEmbeddingAdapter | embed-english-light-v3.0 | 384 |
| CohereEmbeddingAdapter | embed-english-v2.0 | 4096 |
| CohereEmbeddingAdapter | embed-multilingual-v3.0 | 1024 |
What happens if dimensions don't match: If you store memories with one model (e.g., 1536 dims) then switch to another model (e.g., 1024 dims), the storage adapter receives query vectors and stored vectors of different lengths. Cosine similarity between vectors of different dimensions is undefined — results depend on the storage backend's behavior. Most will either error, return empty results, or produce meaningless scores.
Recommendation: Pick one embedding model per storage instance and stick with it. If you need to switch models, create a new storage instance and re-embed from scratch.
DeepSeek users: DeepSeek has no embeddings API. If you use DeepSeek as your LLM, you must either:
- Omit the embedding adapter and use
"recent"or"important"retrieval strategies (no API costs, less precise) - Pair DeepSeek with a separate embedding provider (e.g., OpenAI for embeddings, DeepSeek for chat)
Adapters
MemStack is provider-agnostic. Every boundary is an interface — bring your own LLM, embedding model, and storage backend.
LLM Adapters
Used by summarize() and compileContext(). Ships with OpenAI, Anthropic, Ollama, and Groq built-in — and via baseURL, the OpenAI adapter works with any OpenAI-compatible API (DeepSeek, Mistral, Gemini, Together AI, Perplexity, Fireworks, xAI, and dozens more).
// OpenAI
import { OpenAILLMAdapter } from "@memstack/core";
const llm = new OpenAILLMAdapter({ apiKey: "..." });
// Any OpenAI-compatible API — just change baseURL
const deepseek = new OpenAILLMAdapter({ apiKey: "...", baseURL: "https://api.deepseek.com/v1" });
const mistral = new OpenAILLMAdapter({ apiKey: "...", baseURL: "https://api.mistral.ai/v1" });
const together = new OpenAILLMAdapter({ apiKey: "...", baseURL: "https://api.together.xyz/v1" });
// Anthropic
import { AnthropicLLMAdapter } from "@memstack/core";
const llm = new AnthropicLLMAdapter({
apiKey: process.env.ANTHROPIC_API_KEY!,
defaultModel: "claude-sonnet-4-5-20250929",
});
// Ollama (built-in)
import { OllamaLLMAdapter } from "@memstack/core";
const llm = new OllamaLLMAdapter({
baseURL: "http://localhost:11434",
defaultModel: "llama3.2",
});Embedding Adapters
Used by semantic retrieval. Ships with OpenAI and Cohere built-in — and via baseURL, the OpenAI adapter works with any OpenAI-compatible embedding API (Together AI, Voyage AI, Jina, Nomic, and more).
import { OpenAIEmbeddingAdapter, CohereEmbeddingAdapter } from "@memstack/core";
// OpenAI
new OpenAIEmbeddingAdapter({ apiKey: "...", model: "text-embedding-3-small" }); // 1536 dims
// Cohere
new CohereEmbeddingAdapter({ apiKey: "..." }); // embed-english-v3.0, 1024 dims
// Any OpenAI-compatible embedding API
new OpenAIEmbeddingAdapter({ apiKey: "...", baseURL: "https://api.voyageai.com/v1", model: "voyage-3" });Storage Adapters
MemStack ships with 11 production-ready storage adapters (7 experimental) — every major backend, zero peer dependencies, all client-injected.
Production (e2e verified against real instances)
Built-in (zero external deps):
| Adapter | Backend | Use case |
|---|---|---|
| InMemoryStorageAdapter | In-memory Map | Testing, prototyping |
| DiskStorageAdapter | Local JSON files | Simple local persistence |
| MarkdownStorageAdapter | Append-only .md files | Human-readable, git-diffable, debug-friendly |
| HybridStorageAdapter | Compose any two StorageProviders | Cache + durable, edge + durable |
Relational / SQL:
| Adapter | Backend | Vector search |
|---|---|---|
| PostgresStorageAdapter | PostgreSQL + pgvector | HNSW native |
Vector databases:
| Adapter | Backend |
|---|---|
| QdrantStorageAdapter | Qdrant |
| WeaviateStorageAdapter | Weaviate |
| LanceDBStorageAdapter | LanceDB |
| MongoDBStorageAdapter | MongoDB Atlas Vector Search |
Cache / KV:
| Adapter | Backend |
|---|---|
| RedisStorageAdapter | Redis (ioredis) |
Graph:
| Adapter | Backend |
|---|---|
| Neo4jStorageAdapter | Neo4j |
Experimental (mock-tested, blocked by cloud deps or platform constraints)
Available via direct source import. Not yet in the barrel export — uncomment in src/index.ts when e2e verified.
| Adapter | Backend | Blocker |
|---|---|---|
| SQLiteStorageAdapter | SQLite (better-sqlite3) | Native binary for Node 24 |
| TursoStorageAdapter | Turso (libsql) | Cloud-only (needs Turso account) |
| ChromaStorageAdapter | ChromaDB | Embedding function dependency |
| PineconeStorageAdapter | Pinecone | Cloud-only (needs API key) |
| UpstashStorageAdapter | Upstash Redis + Vector | Cloud-only (needs API key) |
| Mem0StorageAdapter | Mem0 OSS or Cloud | Cloud-only (needs API key) |
| ZepStorageAdapter | Zep Cloud or CE | Cloud-only (needs API key) |
Direct import:
import { ChromaStorageAdapter } from "@memstack/core/src/adapters/storage/chroma.js"
Quick-start per backend:
// Postgres
import { PostgresStorageAdapter } from "@memstack/core";
const storage = new PostgresStorageAdapter({ connectionString: "postgres://..." });
// Redis
import Redis from "ioredis";
import { RedisStorageAdapter } from "@memstack/core";
const storage = new RedisStorageAdapter({ redis: new Redis() });
// Markdown (append-only, human-readable)
import { MarkdownStorageAdapter } from "@memstack/core";
const storage = new MarkdownStorageAdapter({ dir: "./memories" });
// Hybrid (Redis cache + Postgres durable)
import { HybridStorageAdapter } from "@memstack/core";
const storage = new HybridStorageAdapter({
cache: new RedisStorageAdapter({ redis: new Redis() }),
durable: new PostgresStorageAdapter({ connectionString: "postgres://..." }),
});Custom storage:
import type { StorageProvider, MemoryStoreInput } from "@memstack/core";
class MyStorage implements StorageProvider {
async store(input: MemoryStoreInput): Promise<Memory> { /* ... */ }
async get(id: string): Promise<Memory | null> { /* ... */ }
async retrieve(query: MemoryRetrieveQuery, embedding?: number[]): Promise<Memory[]> { /* ... */ }
async count(filter?: MemoryCountFilter): Promise<number> { /* ... */ }
async delete(id: string): Promise<void> { /* ... */ }
async deleteMany(ids: string[]): Promise<number> { /* ... */ }
async storeBatch(inputs: MemoryStoreInput[]): Promise<Memory[]> { /* ... */ }
async initialize(): Promise<void> { /* ... */ }
async close(): Promise<void> { /* ... */ }
}Backend Comparison
| Backend | Vector search | Touch | Status | |---|---|---|---| | InMemory | Cosine in-memory | Yes | ✅ Production | | Disk (JSON) | Keyword + importance | Yes | ✅ Production | | Markdown | Keyword + importance | No | ✅ Production | | Postgres | pgvector HNSW | Yes | ✅ Production | | Redis | RediSearch KNN (auto-detect) | Yes | ✅ Production | | Qdrant | ANN native | No | ✅ Production | | Weaviate | BM25 + vector hybrid | No | ✅ Production | | LanceDB | DiskANN native | No | ✅ Production | | MongoDB | Atlas Vector Search | No | ✅ Production | | Neo4j | Neo4j vector index | No | ✅ Production | | Hybrid | Delegates to cache/durable | If durable supports | ✅ Production | | SQLite | Cosine in-memory | Yes | ✅ Production | | Hybrid | Delegates to cache/durable | If durable supports | ✅ Production |
Full API Reference
MemStack Client
import { MemStack } from "@memstack/core";
const ms = new MemStack({
llm: LLMProvider, // Required — for summarization
embedding?: EmbeddingProvider, // Optional — for semantic search
storage?: StorageProvider, // Optional — defaults to InMemoryStorageAdapter
defaults?: {
summarizationThreshold?: number, // Auto-summarize every N interactions. Default: 100
embedOnStore?: boolean, // Auto-embed on store(). Default: true
pruneStrategy?: PruneStrategy, // Auto-prune on every store(). Default: disabled
},
hooks?: {
onMemoryStored?: (memory: Memory) => void;
onMemoryPruned?: (ids: string[]) => void;
onSummaryCreated?: (summary: Memory, deletedCount: number) => void;
},
});Memory Subsystem
All methods accessible via ms.memory.*:
// Store
ms.memory.store(input: MemoryStoreInput): Promise<Memory>
ms.memory.storeBatch(inputs: MemoryStoreInput[]): Promise<Memory[]>
// Retrieve
ms.memory.retrieve(query: MemoryRetrieveQuery): Promise<Memory[]>
ms.memory.get(id: string): Promise<Memory | null>
// Context assembly
ms.memory.compileContext(options: ContextOptions): Promise<CompiledContext>
// Lifecycle
ms.memory.summarize(options: SummarizeOptions): Promise<{ summary: Memory; deletedCount: number }>
ms.memory.prune(strategy: PruneStrategy): Promise<{ pruned: string[]; count: number }>
ms.memory.dryRunPrune(strategy: PruneStrategy): Promise<{ wouldPrune: string[]; count: number }>
// Management
ms.memory.count(filter?: MemoryCountFilter): Promise<number>
ms.memory.delete(id: string): Promise<void>
ms.memory.deleteMany(ids: string[]): Promise<number>
ms.memory.touch(id: string): Promise<void>
ms.memory.purgeActor(actorId: string): Promise<number>
ms.memory.merge(ids: string[]): Promise<Memory>
ms.memory.stats(actorId?: string): Promise<MemoryStats>
ms.memory.summarizeStream(options: SummarizeOptions): AsyncIterable<{ chunk: string; text: string }>Export / Import
Snapshot and restore full state for persistence, backups, or migration:
import * as fs from "node:fs";
// Save
const snapshot = await ms.export();
fs.writeFileSync("state.json", JSON.stringify(snapshot, null, 2));
// Restore
const data = JSON.parse(fs.readFileSync("state.json", "utf-8"));
await ms2.import(data);Health & Close
const status = await ms.health();
// { storage: true, llm: true, embedding: true }
await ms.close(); // graceful shutdownConfiguration
const ms = new MemStack({
llm: new OpenAILLMAdapter({ apiKey: "..." }),
// Defaults control auto-behavior
defaults: {
summarizationThreshold: 50, // Summarize every 50 interactions (default: 100)
embedOnStore: false, // Don't auto-embed — saves API costs
pruneStrategy: { // Auto-clean on every store()
type: "byAge",
maxAge: 90 * 86400000, // 90 days
},
},
// Hooks for observability
hooks: {
onMemoryStored: (m) => logger.debug("memory:stored", { id: m.id, actor: m.actorId }),
onMemoryPruned: (ids) => logger.info("memory:pruned", { count: ids.length }),
onSummaryCreated: (summary, n) => logger.info("memory:summarized", { count: n }),
},
});Advanced Usage
Custom Storage
Implement StorageProvider for any database. The interface is 9 methods. See the reference section above for the full contract.
Custom LLM / Embedding
Implement LLMProvider or EmbeddingProvider for any service:
import type { LLMProvider } from "@memstack/core";
class TogetherAIAdapter implements LLMProvider {
async complete(req: { system: string; user: string; model?: string }) {
const res = await fetch("https://api.together.xyz/v1/chat/completions", {
headers: { Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json" },
body: JSON.stringify({ model: req.model, messages: [{ role: "system", content: req.system }, { role: "user", content: req.user }] }),
});
const data = await res.json() as any;
return { text: data.choices[0].message.content, tokens: { prompt: data.usage.prompt_tokens, completion: data.usage.completion_tokens, total: data.usage.total_tokens } };
}
}Event Hooks
Monitor memory operations without modifying code:
const ms = new MemStack({
llm,
hooks: {
onMemoryStored: (m) => metrics.increment("memory.stored"),
onSummaryCreated: (_, n) => metrics.gauge("memory.summarized_count", n),
onMemoryPruned: (ids) => metrics.increment("memory.pruned", ids.length),
},
});Development
Setup & Tests
git clone https://github.com/isiomaC/memstack.git
cd memstack
pnpm install
pnpm test # 393 tests, no external services needed
pnpm test:e2e # 82 E2E tests (requires Docker)
pnpm test:watch # Watch mode
pnpm build # CJS + ESM + type declarations
pnpm check # TypeScript type-check onlyDebugging
Use hooks for observability — MemStack has no built-in logging:
const ms = new MemStack({
llm,
hooks: {
onMemoryStored: (m) => console.debug("[memstack] stored:", m.id, m.content.slice(0, 80)),
onMemoryPruned: (ids) => console.debug("[memstack] pruned:", ids.length),
},
});Common issues:
| Symptom | Cause | Fix |
|---------|-------|-----|
| CONFIG_ERROR: LLM provider is required | No LLM adapter | Pass any LLMProvider to config |
| Empty retrieval results | Wrong actorId or no memories stored | Check await ms.memory.count({ actorId }) |
| Semantic search not working | No embedding adapter or embedOnStore: false | Add embedding adapter or use strategy: "recent" |
| High memory usage in production | Using InMemoryStorageAdapter | Implement StorageProvider for Postgres/Redis/etc |
| Poor summarization quality | Default prompt doesn't match your domain | Use summarizationPrompt in defaults config |
Inspecting state at runtime:
// How much data do we have?
const total = await ms.memory.count();
const perActor = await ms.memory.count({ actorId: "user-42" });
// What does one actor's memory look like?
const snapshot = await ms.export();
const actorMemories = snapshot.memories.filter(m => m.actorId === "user-42");
console.log(`User-42: ${actorMemories.length} memories`);
actorMemories.forEach(m => console.log(` [${m.memoryType}] ${m.content.slice(0, 60)} (imp: ${m.importance})`));Publishing to npm
# Bump version, then:
pnpm build && pnpm check && pnpm test
npm login
npm publish --access publicThe @memstack scope requires --access public.
Contributing
Most needed contributions:
- LLM adapters: Google Gemini (native), Amazon Bedrock, Vertex AI
- Embedding adapters: local inference (transformers.js, ONNX)
- Benchmarks: retrieval quality, latency, cost comparisons
- Python port:
pip install memstack
Open an issue or PR at github.com/isiomaC/memstack.
License
MIT © MemStack
