{
  "markdown": "# MemStack\n\n> Implementation priority is maintained in the [canonical roadmap](docs/ROADMAP.md).\n\n> The open-source memory layer for AI agents — store, retrieve, summarize, and prune.\n\n[![npm version](https://img.shields.io/npm/v/@memstack/core)](https://www.npmjs.com/package/@memstack/core)\n[![skills.sh](https://skills.sh/b/isiomaC/memstack)](https://skills.sh/isiomaC/memstack)\n[![MCP Registry](https://img.shields.io/badge/MCP%20Registry-%40memstack%2Fmcp-blueviolet)](https://registry.modelcontextprotocol.io/?q=io.github.isiomaC%2Fmemstack)\n[![CI](https://github.com/isiomaC/memstack/actions/workflows/ci.yml/badge.svg)](https://github.com/isiomaC/memstack/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![MCP Reference](https://img.shields.io/badge/MCP-LLM%20Reference-blue)](https://gitmcp.io/isiomaC/memstack)\n\n```bash\n# Use MemStack in your application\nnpm install @memstack/core\n\n# Give your coding agent the MemStack skill\nnpx skills add isiomaC/memstack\n```\n\n`@memstack/core` is the runtime SDK; the Agent Skill teaches compatible coding agents how to integrate and operate MemStack correctly.\n\n**The 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.\n\n**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.\n\nThink of it as the open-source alternative to [Mem0](https://mem0.ai/) — pluggable storage, bring your own LLM, zero vendor lock-in.\n\n---\n\n## Table of Contents\n\n- [Why MemStack](#why-memstack)\n- [Quick Start](#quick-start)\n- [The Memory Pipeline](#the-memory-pipeline)\n  - [Store](#1-store)\n  - [Retrieve](#2-retrieve)\n  - [Compile Context](#3-compile-context)\n  - [Summarize](#4-summarize)\n  - [Prune](#5-prune)\n- [Real-World Use Cases](#real-world-use-cases)\n  - [Support Agent](#support-agent)\n  - [RAG Pipeline](#rag-pipeline)\n  - [Multi-User Chatbot](#multi-user-chatbot)\n- [Memory Type Reference](#memory-type-reference)\n- [Retrieval Strategies](#retrieval-strategies)\n- [Embeddings](#embeddings)\n- [Adapters](#adapters)\n  - [LLM Adapters](#llm-adapters)\n  - [Embedding Adapters](#embedding-adapters)\n  - [Storage Adapters](#storage-adapters)\n- [Full API Reference](#full-api-reference)\n  - [MemStack Client](#memstack-client)\n  - [Memory Subsystem](#memory-subsystem)\n  - [Export / Import](#export-import)\n  - [Health & Close](#health-close)\n- [Configuration](#configuration)\n- [Advanced Usage](#advanced-usage)\n  - [Custom Storage](#custom-storage)\n  - [Custom LLM / Embedding](#custom-llm-embedding)\n  - [Event Hooks](#event-hooks)\n- [Development](#development)\n  - [Setup & Tests](#setup-tests)\n  - [Debugging](#debugging)\n- [Publishing to npm](#publishing-to-npm)\n- [Contributing](#contributing)\n- [License](#license)\n\n---\n\n## Why MemStack\n\n**LLMs have context windows, not memory.** The difference matters.\n\n| Approach | Problem |\n|----------|---------|\n| **Stuff everything in context** | Cost is O(n²). 100 conversations = thousands of tokens = dollars per call. Quality degrades from \"lost in the middle\" effect. |\n| **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. |\n| **Use Mem0** | Proprietary, cloud-only with their hosted API. You don't control where your data lives. |\n| **Use MemStack** | Full pipeline. Pluggable everything. Your data, your infrastructure. Open source. |\n\n**What MemStack handles that raw vector DBs don't:**\n\n- **Summarization** — compress 100 old interactions into one paragraph, keep meaning, save tokens\n- **Recency weighting** — recent memories matter more; MemStack sorts them higher\n- **Importance scoring** — not all memories are equal; high-importance ones survive pruning\n- **Deduplication** — identical or near-identical memories are collapsed in context assembly\n- **Token budget** — `compileContext()` tells you how many tokens you're spending before the LLM call\n- **Memory-type routing** — interactions, summaries, observations treated differently at retrieval time\n- **Auto-pruning** — old, low-importance memories clean themselves up\n\n---\n\n## Quick Start\n\n```bash\nnpm install @memstack/core\n```\n\n### OpenAI\n\n```typescript\nimport { MemStack, OpenAILLMAdapter, OpenAIEmbeddingAdapter, InMemoryStorageAdapter } from \"@memstack/core\";\n\nconst llm = new OpenAILLMAdapter({ apiKey: process.env.OPENAI_API_KEY! });\n\nconst memstack = new MemStack({\n  llm,\n  embedding: new OpenAIEmbeddingAdapter({ apiKey: process.env.OPENAI_API_KEY! }),\n  storage: new InMemoryStorageAdapter(),\n});\n```\n\n### DeepSeek (no embeddings)\n\nDeepSeek 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.\n\n```typescript\nimport { MemStack, OpenAILLMAdapter, InMemoryStorageAdapter } from \"@memstack/core\";\n\nconst llm = new OpenAILLMAdapter({\n  apiKey: process.env.DEEPSEEK_API_KEY!,\n  baseURL: \"https://api.deepseek.com/v1\",\n  defaultModel: \"deepseek-chat\",\n});\n\nconst memstack = new MemStack({\n  llm,\n  storage: new InMemoryStorageAdapter(),\n  // No embedding adapter — retrieval uses keyword matching\n});\n```\n\n### OpenRouter / Together AI / any OpenAI-compatible API\n\nSame pattern — change `baseURL` and `defaultModel`:\n\n```typescript\n// OpenRouter\nconst llm = new OpenAILLMAdapter({\n  apiKey: process.env.OPENROUTER_API_KEY!,\n  baseURL: \"https://openrouter.ai/api/v1\",\n  defaultModel: \"openai/gpt-4o-mini\",\n});\n\n// Together AI\nconst llm = new OpenAILLMAdapter({\n  apiKey: process.env.TOGETHER_API_KEY!,\n  baseURL: \"https://api.together.xyz/v1\",\n  defaultModel: \"meta-llama/Llama-3.3-70B-Instruct-Turbo\",\n});\n\n// Gemini (OpenAI-compatible endpoint)\nconst llm = new OpenAILLMAdapter({\n  apiKey: process.env.GEMINI_API_KEY!,\n  baseURL: \"https://generativelanguage.googleapis.com/v1beta/openai\",\n  defaultModel: \"gemini-2.0-flash\",\n});\n```\n\n### Store and retrieve\n\n```typescript\n// 1. Store what happened\nawait memstack.memory.store({\n  actorId: \"support-bot-42\",\n  content: \"User reports login failing with error 503 on Chrome 125.\",\n  tags: [\"login\", \"bug\", \"chrome\"],\n  importance: 0.8,\n});\n\n// 2. Later, retrieve relevant context\nconst memories = await memstack.memory.retrieve({\n  actorId: \"support-bot-42\",\n  query: \"login error\",\n  strategy: \"hybrid\",\n});\n\n// 3. Assemble an LLM-ready context\nconst ctx = await memstack.memory.compileContext({\n  actorId: \"support-bot-42\",\n  maxTokens: 2000,\n});\n\nconst response = await llm.complete({\n  system: `You are a support bot. Here is what you remember:\\n${ctx.systemPrompt}`,\n  user: \"The user is back and still can't log in. What do you do?\",\n});\n\nconsole.log(response.text);\n// \"Based on our history, the user has been experiencing 503 errors on Chrome 125...\"\n\n// 4. Every 100 interactions, summarization triggers automatically.\n// Old interactions are compressed into a paragraph. Token costs stay flat.\n```\n\n---\n\n## The Memory Pipeline\n\nMemStack's core is a five-stage pipeline. Each stage can be used independently.\n\n### 1. Store\n\nEvery agent interaction becomes a `Memory` with metadata that controls how it's retrieved, summarized, and pruned later.\n\n```typescript\ninterface Memory {\n  id: string;\n  actorId: string;               // Who this memory belongs to (user ID, agent ID, session ID)\n  memoryType: MemoryType;        // \"interaction\" | \"summary\" | \"observation\" | \"fact\" | \"reflection\"\n  content: string;               // The actual text\n  importance: number;            // 0-1 — higher = survives pruning, ranks higher in retrieval\n  emotionalValence: number;      // -1 to 1 — for tone-aware retrieval\n  tags: string[];                // Filter by tag: \"bug\", \"billing\", \"urgent\", etc.\n  embedding?: number[];          // Computed automatically if embedding adapter is configured\n  metadata?: Record<string, unknown>;  // Your custom fields\n  expiresAt?: Date;              // Auto-pruned after this date\n  sourceId?: string;             // Link back to the originating event\n  createdAt: Date;\n}\n```\n\n```typescript\n// Simple store\nawait ms.memory.store({\n  actorId: \"agent-7\",\n  content: \"Customer asked about refund policy for Q2 purchases.\",\n  tags: [\"billing\", \"refund\"],\n});\n\n// Batch store — embeddings are batched into one API call for efficiency\nawait ms.memory.storeBatch([\n  { actorId: \"agent-7\", content: \"First interaction\" },\n  { actorId: \"agent-7\", content: \"Second interaction\" },\n  { actorId: \"agent-7\", content: \"Third interaction\" },\n]);\n```\n\n### 2. Retrieve\n\nPull back what's relevant — by keyword, by meaning (semantic), by recency, or by importance.\n\n```typescript\nconst memories = await ms.memory.retrieve({\n  actorId: \"agent-7\",              // Scope to one actor\n  query: \"refund policy\",          // What to search for\n  strategy: \"hybrid\",              // How to rank: \"recent\" | \"important\" | \"semantic\" | \"hybrid\"\n  limit: 10,                       // Max results\n  memoryTypes: [\"interaction\"],    // Only certain types\n  tags: [\"billing\"],               // Only certain tags\n});\n```\n\n**Strategy behavior:**\n\n| Strategy | Sorts by | Requires embeddings | Best for |\n|----------|----------|--------------------|----------|\n| `recent` | Newest first | No | Knowing what just happened |\n| `important` | Highest importance first | No | Filtering noise, keeping signal |\n| `semantic` | Cosine similarity to query | Yes | \"Find memories about X\" |\n| `hybrid` | Semantic + importance blend | Yes | Best of both worlds |\n\nNo embedding adapter? `semantic` and `hybrid` fall back to keyword matching + importance sort. No API costs, just less precise.\n\n### 3. Compile Context\n\n`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.\n\n```typescript\nconst ctx = await ms.memory.compileContext({\n  actorId: \"agent-7\",\n  maxTokens: 2000,               // Budget — assembler stops when it hits this\n  memoryTypes: [\"interaction\", \"summary\"],\n});\n\n// ctx.systemPrompt:\n// ## Important Memories\n// - The customer has been attempting login for 3 days. (importance: 0.85)\n// - Refund was processed for order #4521 on Jan 12. (importance: 0.72)\n// \n// ## Recent Interactions\n// - Customer asked about refund policy for Q2 purchases.\n// - Customer reported login error 503 on Chrome 125.\n\nconsole.log(ctx.tokenEstimate);  // ~280\n\n// Inject into your LLM call\nconst currentMessage = \"The user is asking about their refund status.\";\nconst response = await llm.complete({\n  system: ctx.systemPrompt,\n  user: currentMessage,\n});\n\nconsole.log(response.text);\n```\n\n`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.\n\n### 4. Summarize\n\nWhen 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.\n\n```typescript\nconst { summary, deletedCount } = await ms.memory.summarize({\n  actorId: \"agent-7\",\n  olderThan: new Date(Date.now() - 7 * 86400000),  // Older than 7 days\n  skipMostRecent: 10,        // Never touch the 10 most recent\n  targetCount: 50,           // Summarize at most 50 memories\n  memoryTypes: [\"interaction\"],\n  keepOriginals: false,      // Delete originals after summary\n});\n\n// summary.content:\n// \"Over the past week, the customer reported recurring login failures (error 503)\n//  on Chrome 125. Multiple troubleshooting attempts including cache clearing and \n//  password reset were unsuccessful. A refund was processed for order #4521.\"\n\nconsole.log(deletedCount);   // 47 — 47 interactions compressed into 1 summary memory\n```\n\n**Auto-summarization:** Set `summarizationThreshold` in config (default: 100). Every 100th interaction for an actor triggers summarization automatically.\n\n**Warning:** `keepOriginals: false` deletes the summarized memories. Set `keepOriginals: true` to preserve them alongside the summary.\n\n**Custom summarization prompt:**\n\n```typescript\nconst ms = new MemStack({\n  llm,\n  defaults: {\n    summarizationPrompt:\n      \"You are an enterprise support memory compressor. Highlight: customer name,\n       product, severity, resolution status, and any open issues.\",\n  },\n});\n```\n\n### 5. Prune\n\nNot all memories deserve to live forever. Pruning removes low-value memories to keep storage and retrieval fast.\n\n```typescript\n// Remove memories older than 30 days\nawait ms.memory.prune({ type: \"byAge\", maxAge: 30 * 86400000 });\n\n// Keep only memories above importance 0.3\nawait ms.memory.prune({ type: \"byImportance\", minImportance: 0.3 });\n\n// Keep at most 500 memories per actor\nawait ms.memory.prune({ type: \"byCount\", maxPerActor: 500 });\n\n// Remove specific types\nawait ms.memory.prune({ type: \"byType\", memoryTypes: [\"observation\"] });\n\n// Custom logic\nawait ms.memory.prune({\n  type: \"custom\",\n  shouldRemove: (memory) => memory.content.includes(\"[RESOLVED]\"),\n});\n\n// Dry run first — see what would be removed\nconst { wouldPrune, count } = await ms.memory.dryRunPrune({\n  type: \"byAge\",\n  maxAge: 86400000,\n});\nconsole.log(`Would remove ${count} memories:`, wouldPrune);\n```\n\nAuto-prune on every `process()` call by setting `pruneStrategy` in config:\n\n```typescript\nconst ms = new MemStack({\n  llm,\n  defaults: {\n    pruneStrategy: { type: \"byImportance\", minImportance: 0.05 },\n  },\n});\n```\n\n---\n\n## Real-World Use Cases\n\n### Support Agent\n\n```typescript\n// detectUrgency and classifyIntent are your own business logic.\n// They could be simple keyword matchers, regex, or an LLM call.\nfunction detectUrgency(msg: string): number {\n  if (msg.match(/urgent|asap|immediately/i)) return 0.9;\n  if (msg.match(/error|fail|broken/i)) return 0.7;\n  return 0.5;\n}\n\nfunction classifyIntent(msg: string): string[] {\n  const tags: string[] = [];\n  if (msg.match(/bill|refund|charge|payment/i)) tags.push(\"billing\");\n  if (msg.match(/error|bug|fail|crash/i)) tags.push(\"bug\");\n  if (msg.match(/login|password|account/i)) tags.push(\"account\");\n  return tags;\n}\n\n// Every customer message becomes a memory\nasync function handleMessage(customerId: string, message: string) {\n  await ms.memory.store({\n    actorId: `customer:${customerId}`,\n    content: message,\n    importance: detectUrgency(message),\n    tags: classifyIntent(message),\n  });\n\n  // Retrieve everything relevant to this customer's history\n  const ctx = await ms.memory.compileContext({\n    actorId: `customer:${customerId}`,\n    maxTokens: 1500,\n  });\n\n  const response = await llm.complete({\n    system: `You are a support agent. Customer history:\\n${ctx.systemPrompt}`,\n    user: message,\n  });\n\n  return response.text;\n}\n\n// Every 100th interaction, old history auto-compresses.\n// A customer with 10,000 messages still fits in a $0.02 LLM call.\n```\n\n### RAG Pipeline\n\n```typescript\n// Suppose you have documents from your knowledge base\nconst documents = [\n  { text: \"Authentication uses JWT tokens with 15-minute expiry.\", url: \"/docs/auth\", section: \"security\" },\n  { text: \"Refunds are processed within 5-10 business days.\", url: \"/docs/billing\", section: \"billing\" },\n];\n\n// Index documents as observation memories\nfor (const doc of documents) {\n  await ms.memory.store({\n    actorId: \"knowledge-base\",\n    content: doc.text,\n    memoryType: \"observation\",\n    metadata: { source: doc.url, section: doc.section },\n  });\n}\n\n// Query with semantic search\nconst relevantDocs = await ms.memory.retrieve({\n  actorId: \"knowledge-base\",\n  query: \"How does authentication work?\",\n  strategy: \"semantic\",\n  limit: 5,\n});\n\nconst ctx = await ms.memory.compileContext({\n  actorId: \"knowledge-base\",\n  memoryTypes: [\"observation\"],\n});\n\n// Prompt the LLM with retrieved context\nconst answer = await llm.complete({\n  system: `Answer using only these documents:\\n${ctx.systemPrompt}`,\n  user: \"How does authentication work?\",\n});\n```\n\n### Multi-User Chatbot\n\n```typescript\n// Each user gets their own memory space\nasync function chat(userId: string, message: string) {\n  await ms.memory.store({\n    actorId: userId,\n    content: message,\n  });\n\n  const ctx = await ms.memory.compileContext({\n    actorId: userId,\n    maxTokens: 1000,\n  });\n\n  return llm.complete({\n    system: `You are a friendly assistant. Conversation history with this user:\\n${ctx.systemPrompt}`,\n    user: message,\n  });\n}\n\n// Get stats\nconst total = await ms.memory.count();\nconst userCount = await ms.memory.count({ actorId: \"user-42\" });\n```\n\n---\n\n## Memory Type Reference\n\n| Type | Purpose | Example |\n|------|---------|---------|\n| `interaction` | Default. Direct exchanges between agent and user/other agent. | \"User asked about billing.\" |\n| `summary` | Compressed collection of old interactions. Created by `summarize()`. | \"Over 3 weeks, user reported 5 login failures...\" |\n| `observation` | Passive knowledge — facts, documents, things the agent knows but didn't interact with. | \"Company refund policy is 30 days from purchase.\" |\n| `fact` | Verified knowledge — discrete truths the agent has confirmed. | \"The user's subscription tier is Enterprise.\" |\n| `reflection` | Self-generated insight — the agent thinking about its own experiences. | \"I tend to over-explain billing policies — should be more concise.\" |\n\nTypes control retrieval behavior — `compileContext()` treats `interaction` and `summary` differently from `observation`. Use types to separate \"what happened\" from \"what I know.\"\n\n---\n\n## Retrieval Strategies\n\nFour strategies, each with a purpose:\n\n```typescript\n// \"What just happened?\" — most recent first\nawait ms.memory.retrieve({ actorId: \"x\", strategy: \"recent\", limit: 3 });\n\n// \"What matters most?\" — highest importance, ignoring age\nawait ms.memory.retrieve({ actorId: \"x\", strategy: \"important\" });\n\n// \"What relates to this query?\" — cosine similarity search (needs embeddings)\nawait ms.memory.retrieve({ actorId: \"x\", query: \"login bug\", strategy: \"semantic\" });\n\n// \"Balance relevance and importance\" — semantic + importance blend\nawait ms.memory.retrieve({ actorId: \"x\", query: \"login bug\", strategy: \"hybrid\" });\n```\n\n**Choosing a strategy:**\n- Use `recent` for chatbots, ongoing conversations, anything time-sensitive\n- Use `important` for long-running agents where signal-to-noise matters\n- Use `semantic` for RAG, document search, knowledge base queries\n- Use `hybrid` for most agent memory — it balances meaning with significance\n\n---\n\n## Embeddings\n\nEmbeddings power semantic search. They're optional — without them, retrieval uses keyword matching.\n\n### With embeddings vs Without embeddings\n\n**With embeddings** (`embedding` adapter configured):\n\n```typescript\nimport { MemStack, OpenAILLMAdapter, OpenAIEmbeddingAdapter, InMemoryStorageAdapter } from \"@memstack/core\";\n\nconst ms = new MemStack({\n  llm: new OpenAILLMAdapter({ apiKey: process.env.OPENAI_API_KEY! }),\n  embedding: new OpenAIEmbeddingAdapter({ apiKey: process.env.OPENAI_API_KEY! }),\n  storage: new InMemoryStorageAdapter(),\n});\n\n// store() computes a 1536-dim vector automatically\nawait ms.memory.store({\n  actorId: \"agent-7\",\n  content: \"Customer asked about refund policy for Q2 purchases.\",\n});\n\n// retrieve() with \"semantic\" or \"hybrid\" uses cosine similarity\n// Query: \"refund\" finds the refund policy memory even though the word \"refund\"\n// appears differently across stored memories.\nconst results = await ms.memory.retrieve({\n  actorId: \"agent-7\",\n  query: \"how do I get my money back\",\n  strategy: \"semantic\",\n});\n// Matches \"Customer asked about refund policy\" — semantic match, not keyword match.\n```\n\n**Without embeddings** (no `embedding` adapter):\n\n```typescript\nconst ms = new MemStack({\n  llm: new OpenAILLMAdapter({ apiKey: process.env.OPENAI_API_KEY! }),\n  storage: new InMemoryStorageAdapter(),\n  // no embedding adapter\n});\n\n// store() works identically, just no vector computed\nawait ms.memory.store({\n  actorId: \"agent-7\",\n  content: \"Customer asked about refund policy for Q2 purchases.\",\n});\n\n// retrieve() with \"semantic\" or \"hybrid\" falls back to keyword matching\n// plus importance/recency sorting. No API costs, no setup required.\nconst results = await ms.memory.retrieve({\n  actorId: \"agent-7\",\n  query: \"refund\",\n  strategy: \"hybrid\", // falls back to keyword + importance\n});\n// Still works — finds \"refund\" via substring match. Less precise for\n// paraphrased queries (\"money back\" won't match \"refund\").\n```\n\n**Batch embedding:** `storeBatch()` sends all texts in one embedding API call, reducing cost and latency.\n\n```typescript\n// Disable auto-embedding if you only need keyword search\nconst ms = new MemStack({\n  llm,\n  embedding: new OpenAIEmbeddingAdapter({ apiKey }),\n  defaults: { embedOnStore: false },\n});\n```\n\n### Vector dimensions and model compatibility\n\nDifferent 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.\n\n| Adapter | Default model | Dimensions |\n|---------|--------------|------------|\n| `OpenAIEmbeddingAdapter` | `text-embedding-3-small` | 1536 |\n| `OpenAIEmbeddingAdapter` | `text-embedding-3-large` | 3072 |\n| `CohereEmbeddingAdapter` | `embed-english-v3.0` | 1024 |\n| `CohereEmbeddingAdapter` | `embed-english-light-v3.0` | 384 |\n| `CohereEmbeddingAdapter` | `embed-english-v2.0` | 4096 |\n| `CohereEmbeddingAdapter` | `embed-multilingual-v3.0` | 1024 |\n\n**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.\n\n**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.\n\n**DeepSeek users:** DeepSeek has no embeddings API. If you use DeepSeek as your LLM, you must either:\n1. Omit the embedding adapter and use `\"recent\"` or `\"important\"` retrieval strategies (no API costs, less precise)\n2. Pair DeepSeek with a separate embedding provider (e.g., OpenAI for embeddings, DeepSeek for chat)\n\n\n---\n\n## Adapters\n\nMemStack is provider-agnostic. Every boundary is an interface — bring your own LLM, embedding model, and storage backend.\n\n### LLM Adapters\n\nUsed 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).\n\n```typescript\n// OpenAI\nimport { OpenAILLMAdapter } from \"@memstack/core\";\nconst llm = new OpenAILLMAdapter({ apiKey: \"...\" });\n\n// Any OpenAI-compatible API — just change baseURL\nconst deepseek = new OpenAILLMAdapter({ apiKey: \"...\", baseURL: \"https://api.deepseek.com/v1\" });\nconst mistral = new OpenAILLMAdapter({ apiKey: \"...\", baseURL: \"https://api.mistral.ai/v1\" });\nconst together = new OpenAILLMAdapter({ apiKey: \"...\", baseURL: \"https://api.together.xyz/v1\" });\n\n// Anthropic\nimport { AnthropicLLMAdapter } from \"@memstack/core\";\nconst llm = new AnthropicLLMAdapter({\n  apiKey: process.env.ANTHROPIC_API_KEY!,\n  defaultModel: \"claude-sonnet-4-5-20250929\",\n});\n\n// Ollama (built-in)\nimport { OllamaLLMAdapter } from \"@memstack/core\";\nconst llm = new OllamaLLMAdapter({\n  baseURL: \"http://localhost:11434\",\n  defaultModel: \"llama3.2\",\n});\n```\n\n### Embedding Adapters\n\nUsed 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).\n\n```typescript\nimport { OpenAIEmbeddingAdapter, CohereEmbeddingAdapter } from \"@memstack/core\";\n\n// OpenAI\nnew OpenAIEmbeddingAdapter({ apiKey: \"...\", model: \"text-embedding-3-small\" }); // 1536 dims\n\n// Cohere\nnew CohereEmbeddingAdapter({ apiKey: \"...\" }); // embed-english-v3.0, 1024 dims\n\n// Any OpenAI-compatible embedding API\nnew OpenAIEmbeddingAdapter({ apiKey: \"...\", baseURL: \"https://api.voyageai.com/v1\", model: \"voyage-3\" });\n```\n\n### Storage Adapters\n\nMemStack contains 18 storage-adapter implementations. Twelve are exported from `@memstack/core`; six remain experimental source implementations. Core has no runtime dependencies, and database clients are injected by callers.\n\n**Support levels:**\n\n- **Production-ready** means exported from the public package, covered by unit tests, and supported as part of the public API.\n- **Real-service E2E verified** means the adapter also passes against its actual database implementation in `pnpm test:e2e`.\n- **Mock-tested** means unit coverage uses an injected fake client rather than a live cloud service.\n- **Experimental** means implemented in source but not exported from the published package.\n\n### Public package exports\n\n**Built-in (zero external deps):**\n| Adapter | Backend | Use case |\n|---|---|---|\n| `InMemoryStorageAdapter` | In-memory Map | Testing, prototyping |\n| `DiskStorageAdapter` | Local JSON files | Simple local persistence |\n| `MarkdownStorageAdapter` | Append-only .md files | Human-readable, git-diffable, debug-friendly |\n| `HybridStorageAdapter` | Compose any two StorageProviders | Cache + durable, edge + durable |\n\n**Relational / SQL:**\n| Adapter | Backend | Vector search |\n|---|---|---|\n| `PostgresStorageAdapter` | PostgreSQL + pgvector | HNSW native |\n| `SQLiteStorageAdapter` | SQLite (better-sqlite3) | Cosine in-memory |\n\n**Vector databases:**\n| Adapter | Backend |\n|---|---|\n| `QdrantStorageAdapter` | Qdrant |\n| `WeaviateStorageAdapter` | Weaviate |\n| `LanceDBStorageAdapter` | LanceDB |\n| `MongoDBStorageAdapter` | MongoDB Atlas Vector Search |\n\n**Cache / KV:**\n| Adapter | Backend |\n|---|---|\n| `RedisStorageAdapter` | Redis (ioredis) |\n\n**Graph:**\n| Adapter | Backend |\n|---|---|\n| `Neo4jStorageAdapter` | Neo4j |\n\n### Experimental (mock-tested or missing an optional E2E capability)\n\nThese implementations are available to source contributors but are not part of the published package API.\n\n| Adapter | Backend | Blocker |\n|---|---|---|\n| `TursoStorageAdapter` | Turso (libsql) | Cloud-only (needs Turso account) |\n| `ChromaStorageAdapter` | ChromaDB | Embedding function dependency |\n| `PineconeStorageAdapter` | Pinecone | Cloud-only (needs API key) |\n| `UpstashStorageAdapter` | Upstash Redis + Vector | Cloud-only (needs API key) |\n| `Mem0StorageAdapter` | Mem0 OSS or Cloud | Cloud-only (needs API key) |\n| `ZepStorageAdapter` | Zep Cloud or CE | Cloud-only (needs API key) |\n\nLive cloud compatibility remains unverified for Pinecone, Upstash, Mem0, Zep, and Turso. Chroma's real-client E2E suite is skipped when its optional default embedding function is unavailable. LLM and embedding-provider tests use mocks; live-provider testing is opt-in and is not part of CI.\n\n**Quick-start per backend:**\n\n```ts\n// Postgres\nimport { PostgresStorageAdapter } from \"@memstack/core\";\nconst storage = new PostgresStorageAdapter({ connectionString: \"postgres://...\" });\n\n// Redis\nimport Redis from \"ioredis\";\nimport { RedisStorageAdapter } from \"@memstack/core\";\nconst storage = new RedisStorageAdapter({ redis: new Redis() });\n\n// Markdown (append-only, human-readable)\nimport { MarkdownStorageAdapter } from \"@memstack/core\";\nconst storage = new MarkdownStorageAdapter({ dir: \"./memories\" });\n\n// Hybrid (Redis cache + Postgres durable)\nimport { HybridStorageAdapter } from \"@memstack/core\";\nconst storage = new HybridStorageAdapter({\n  cache: new RedisStorageAdapter({ redis: new Redis() }),\n  durable: new PostgresStorageAdapter({ connectionString: \"postgres://...\" }),\n});\n```\n\n**Custom storage:**\n```ts\nimport type { StorageProvider, MemoryStoreInput } from \"@memstack/core\";\n\nclass MyStorage implements StorageProvider {\n  async store(input: MemoryStoreInput): Promise<Memory> { /* ... */ }\n  async get(id: string): Promise<Memory | null> { /* ... */ }\n  async retrieve(query: MemoryRetrieveQuery, embedding?: number[]): Promise<Memory[]> { /* ... */ }\n  async count(filter?: MemoryCountFilter): Promise<number> { /* ... */ }\n  async delete(id: string): Promise<void> { /* ... */ }\n  async deleteMany(ids: string[]): Promise<number> { /* ... */ }\n  async storeBatch(inputs: MemoryStoreInput[]): Promise<Memory[]> { /* ... */ }\n  async initialize(): Promise<void> { /* ... */ }\n  async close(): Promise<void> { /* ... */ }\n}\n```\n\n---\n\n## Backend Comparison\n\n| Backend | Vector search | Touch | Status |\n|---|---|---|---|\n| InMemory | Cosine in-memory | Yes | ✅ Production |\n| Disk (JSON) | Keyword + importance | Yes | ✅ Production |\n| Markdown | Keyword + importance | No | ✅ Production |\n| Postgres | pgvector HNSW | Yes | ✅ Production |\n| Redis | RediSearch KNN (auto-detect) | Yes | ✅ Production |\n| Qdrant | ANN native | No | ✅ Production |\n| Weaviate | BM25 + vector hybrid | No | ✅ Production |\n| LanceDB | DiskANN native | No | ✅ Production |\n| MongoDB | Atlas Vector Search | No | ✅ Production |\n| Neo4j | Neo4j vector index | No | ✅ Production |\n| Hybrid | Delegates to cache/durable | If durable supports | ✅ Production |\n| SQLite | Cosine in-memory | Yes | ✅ Production |\n\n---\n\n## Full API Reference\n\n### MemStack Client\n\n```typescript\nimport { MemStack } from \"@memstack/core\";\n\nconst ms = new MemStack({\n  llm: LLMProvider,                    // Required — for summarization\n  embedding?: EmbeddingProvider,       // Optional — for semantic search\n  storage?: StorageProvider,           // Optional — defaults to InMemoryStorageAdapter\n  defaults?: {\n    summarizationThreshold?: number,   // Auto-summarize every N process() calls. Default: 100\n    embedOnStore?: boolean,            // Auto-embed on store(). Default: true\n    pruneStrategy?: PruneStrategy,     // Auto-prune during process() (throttled). Default: disabled\n    pruneInterval?: number,            // Run auto-prune every N process() calls. Default: 100\n    autoImportance?: boolean,          // LLM-score importance in process() when not provided. Default: false\n    autoTags?: boolean,                // LLM-extract tags in process() when not provided. Default: false\n    summarizationPrompt?: string,      // Custom prompt for the summarizer\n  },\n  hooks?: {\n    onMemoryStored?: (memory: Memory) => void;\n    onMemoryPruned?: (ids: string[]) => void;\n    onSummaryCreated?: (summary: Memory, deletedCount: number) => void;\n    onError?: (error: Error, context: string) => void;\n  },\n});\n```\n\n> **Auto-behaviors run inside `process()`, not `store()`.** `process()` tracks a\n> per-actor call count: summarization fires every `summarizationThreshold` calls,\n> and pruning fires every `pruneInterval` calls (when `pruneStrategy` is set).\n> `store()` is the low-level write and never triggers these.\n\n### Memory Subsystem\n\nAll methods accessible via `ms.memory.*`:\n\n```typescript\n// Store\nms.memory.store(input: MemoryStoreInput): Promise<Memory>\nms.memory.storeBatch(inputs: MemoryStoreInput[]): Promise<Memory[]>\n\n// Retrieve\nms.memory.retrieve(query: MemoryRetrieveQuery): Promise<Memory[]>\nms.memory.get(id: string): Promise<Memory | null>\n\n// Context assembly\nms.memory.compileContext(options: ContextOptions): Promise<CompiledContext>\n\n// Lifecycle\nms.memory.summarize(options: SummarizeOptions): Promise<{ summary: Memory; deletedCount: number }>\nms.memory.prune(strategy: PruneStrategy): Promise<{ pruned: string[]; count: number }>\nms.memory.dryRunPrune(strategy: PruneStrategy): Promise<{ wouldPrune: string[]; count: number }>\n\n// Management\nms.memory.count(filter?: MemoryCountFilter): Promise<number>\nms.memory.delete(id: string): Promise<void>\nms.memory.deleteMany(ids: string[]): Promise<number>\nms.memory.touch(id: string): Promise<void>\nms.memory.purgeActor(actorId: string): Promise<number>\nms.memory.merge(ids: string[]): Promise<Memory>\nms.memory.stats(actorId?: string): Promise<MemoryStats>\nms.memory.summarizeStream(options: SummarizeOptions): AsyncIterable<{ chunk: string; text: string }>\n```\n\n### Export / Import\n\nSnapshot and restore full state for persistence, backups, or migration:\n\n```typescript\nimport * as fs from \"node:fs\";\n\n// Save\nconst snapshot = await ms.export();\nfs.writeFileSync(\"state.json\", JSON.stringify(snapshot, null, 2));\n\n// Restore\nconst data = JSON.parse(fs.readFileSync(\"state.json\", \"utf-8\"));\nawait ms2.import(data);\n```\n\nEach memory's original `createdAt` is preserved on import, so `export` → `import` is a lossless round-trip — safe for backups and cross-backend migration (e.g. disk → Postgres). All storage adapters honor a `createdAt` supplied on `store()`/`storeBatch()`; when omitted, they default to the current time.\n\n### Health & Close\n\n```typescript\nconst status = await ms.health();\n// { storage: true, llm: true, embedding: true }\n\nawait ms.close(); // graceful shutdown\n```\n\n---\n\n## Configuration\n\n```typescript\nconst ms = new MemStack({\n  llm: new OpenAILLMAdapter({ apiKey: \"...\" }),\n\n  // Defaults control auto-behavior (all applied during process())\n  defaults: {\n    summarizationThreshold: 50,      // Summarize every 50 process() calls (default: 100)\n    embedOnStore: false,             // Don't auto-embed — saves API costs\n    pruneStrategy: {                 // Auto-clean during process(), throttled by pruneInterval\n      type: \"byAge\",\n      maxAge: 90 * 86400000,         // 90 days\n    },\n    pruneInterval: 100,              // Run the prune check every 100 process() calls (default: 100)\n    autoImportance: true,            // Let the LLM score importance when you don't pass one\n    autoTags: true,                  // Let the LLM extract tags when you don't pass any\n  },\n\n  // Hooks for observability\n  hooks: {\n    onMemoryStored: (m) => logger.debug(\"memory:stored\", { id: m.id, actor: m.actorId }),\n    onMemoryPruned: (ids) => logger.info(\"memory:pruned\", { count: ids.length }),\n    onSummaryCreated: (summary, n) => logger.info(\"memory:summarized\", { count: n }),\n    onError: (err, context) => logger.error(\"memory:error\", { context, message: err.message }),\n  },\n});\n```\n\n---\n\n## Advanced Usage\n\n### Custom Storage\n\nImplement `StorageProvider` for any database. The interface is 9 methods. See the reference section above for the full contract.\n\n### Custom LLM / Embedding\n\nImplement `LLMProvider` or `EmbeddingProvider` for any service:\n\n```typescript\nimport type { LLMProvider } from \"@memstack/core\";\n\nclass TogetherAIAdapter implements LLMProvider {\n  async complete(req: { system: string; user: string; model?: string }) {\n    const res = await fetch(\"https://api.together.xyz/v1/chat/completions\", {\n      headers: { Authorization: `Bearer ${this.apiKey}`, \"Content-Type\": \"application/json\" },\n      body: JSON.stringify({ model: req.model, messages: [{ role: \"system\", content: req.system }, { role: \"user\", content: req.user }] }),\n    });\n    const data = await res.json() as any;\n    return { text: data.choices[0].message.content, tokens: { prompt: data.usage.prompt_tokens, completion: data.usage.completion_tokens, total: data.usage.total_tokens } };\n  }\n}\n```\n\n### Event Hooks\n\nMonitor memory operations without modifying code:\n\n```typescript\nconst ms = new MemStack({\n  llm,\n  hooks: {\n    onMemoryStored: (m) => metrics.increment(\"memory.stored\"),\n    onSummaryCreated: (_, n) => metrics.gauge(\"memory.summarized_count\", n),\n    onMemoryPruned: (ids) => metrics.increment(\"memory.pruned\", ids.length),\n  },\n});\n```\n\n---\n\n---\n\n## Development\n\n### Setup & Tests\n\n```bash\ngit clone https://github.com/isiomaC/memstack.git\ncd memstack\npnpm install\n\npnpm test             # 407 core tests, no external services needed\npnpm test:packages    # 78 package tests after dependency-ordered builds\npnpm test:e2e         # 80 pass, 1 optional Chroma skip (requires Docker services)\npnpm test:e2e:run     # Start services, run E2E once, preserve failure logs, clean up\npnpm smoke:artifacts  # Built core, CLI, MCP, and server black-box checks\npnpm smoke:packages   # Pack and install publishable tarballs in a clean project\npnpm smoke:docker     # Build and exercise the server image\npnpm verify           # Complete local verification pipeline\npnpm test:watch       # Watch core tests\npnpm build:all        # Build core and all workspace packages\npnpm check:all        # Type-check core and all workspace packages\n```\n\nCI exposes a stable `verification` job. Configure that job as a required status check in GitHub branch protection for `main`.\n\n### Debugging\n\nUse hooks for observability — MemStack has no built-in logging:\n\n```typescript\nconst ms = new MemStack({\n  llm,\n  hooks: {\n    onMemoryStored: (m) => console.debug(\"[memstack] stored:\", m.id, m.content.slice(0, 80)),\n    onMemoryPruned: (ids) => console.debug(\"[memstack] pruned:\", ids.length),\n  },\n});\n```\n\n**Common issues:**\n\n| Symptom | Cause | Fix |\n|---------|-------|-----|\n| `CONFIG_ERROR: LLM provider is required` | No LLM adapter | Pass any `LLMProvider` to config |\n| Empty retrieval results | Wrong `actorId` or no memories stored | Check `await ms.memory.count({ actorId })` |\n| Semantic search not working | No embedding adapter or `embedOnStore: false` | Add embedding adapter or use `strategy: \"recent\"` |\n| High memory usage in production | Using InMemoryStorageAdapter | Implement `StorageProvider` for Postgres/Redis/etc |\n| Poor summarization quality | Default prompt doesn't match your domain | Use `summarizationPrompt` in `defaults` config |\n\n**Inspecting state at runtime:**\n\n```typescript\n// How much data do we have?\nconst total = await ms.memory.count();\nconst perActor = await ms.memory.count({ actorId: \"user-42\" });\n\n// What does one actor's memory look like?\nconst snapshot = await ms.export();\nconst actorMemories = snapshot.memories.filter(m => m.actorId === \"user-42\");\nconsole.log(`User-42: ${actorMemories.length} memories`);\nactorMemories.forEach(m => console.log(`  [${m.memoryType}] ${m.content.slice(0, 60)} (imp: ${m.importance})`));\n```\n\n---\n\n## Publishing to npm\n\n```bash\n# Bump version, then:\npnpm build && pnpm check && pnpm test\nnpm login\nnpm publish --access public\n```\n\nThe `@memstack` scope requires `--access public`.\n\n---\n\n## Contributing\n\nMost needed contributions:\n\n- **LLM adapters**: Google Gemini (native), Amazon Bedrock, Vertex AI\n- **Embedding adapters**: local inference (transformers.js, ONNX)\n- **Benchmarks**: retrieval quality, latency, cost comparisons\n- **Python port**: `pip install memstack`\n\nOpen an issue or PR at [github.com/isiomaC/memstack](https://github.com/isiomaC/memstack).\n\n---\n\n## License\n\nMIT © [MemStack](https://github.com/isiomaC/memstack)\n",
  "bytes": 39370,
  "sha": "2b0f60187582b93bd08aa3a10a6827290c8fbf7b1d2d8129ad034c0c3b020729",
  "repo_slug": "isiomac/memstack",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_isiomac_memstack_ba1bc7bd/readme"
}