@memstack/core 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -82,15 +82,74 @@ Think of it as the open-source alternative to [Mem0](https://mem0.ai/) — plugg
82
82
 
83
83
  ## Quick Start
84
84
 
85
+ ```bash
86
+ npm install @memstack/core
87
+ ```
88
+
89
+ ### OpenAI
90
+
85
91
  ```typescript
86
92
  import { MemStack, OpenAILLMAdapter, OpenAIEmbeddingAdapter, InMemoryStorageAdapter } from "@memstack/core";
87
93
 
94
+ const llm = new OpenAILLMAdapter({ apiKey: process.env.OPENAI_API_KEY! });
95
+
88
96
  const memstack = new MemStack({
89
- llm: new OpenAILLMAdapter({ apiKey: process.env.OPENAI_API_KEY! }),
97
+ llm,
90
98
  embedding: new OpenAIEmbeddingAdapter({ apiKey: process.env.OPENAI_API_KEY! }),
91
99
  storage: new InMemoryStorageAdapter(),
92
100
  });
101
+ ```
102
+
103
+ ### DeepSeek (no embeddings)
104
+
105
+ 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.
106
+
107
+ ```typescript
108
+ import { MemStack, OpenAILLMAdapter, InMemoryStorageAdapter } from "@memstack/core";
109
+
110
+ const llm = new OpenAILLMAdapter({
111
+ apiKey: process.env.DEEPSEEK_API_KEY!,
112
+ baseURL: "https://api.deepseek.com/v1",
113
+ defaultModel: "deepseek-chat",
114
+ });
115
+
116
+ const memstack = new MemStack({
117
+ llm,
118
+ storage: new InMemoryStorageAdapter(),
119
+ // No embedding adapter — retrieval uses keyword matching
120
+ });
121
+ ```
93
122
 
123
+ ### OpenRouter / Together AI / any OpenAI-compatible API
124
+
125
+ Same pattern — change `baseURL` and `defaultModel`:
126
+
127
+ ```typescript
128
+ // OpenRouter
129
+ const llm = new OpenAILLMAdapter({
130
+ apiKey: process.env.OPENROUTER_API_KEY!,
131
+ baseURL: "https://openrouter.ai/api/v1",
132
+ defaultModel: "openai/gpt-4o-mini",
133
+ });
134
+
135
+ // Together AI
136
+ const llm = new OpenAILLMAdapter({
137
+ apiKey: process.env.TOGETHER_API_KEY!,
138
+ baseURL: "https://api.together.xyz/v1",
139
+ defaultModel: "meta-llama/Llama-3.3-70B-Instruct-Turbo",
140
+ });
141
+
142
+ // Gemini (OpenAI-compatible endpoint)
143
+ const llm = new OpenAILLMAdapter({
144
+ apiKey: process.env.GEMINI_API_KEY!,
145
+ baseURL: "https://generativelanguage.googleapis.com/v1beta/openai",
146
+ defaultModel: "gemini-2.0-flash",
147
+ });
148
+ ```
149
+
150
+ ### Store and retrieve
151
+
152
+ ```typescript
94
153
  // 1. Store what happened
95
154
  await memstack.memory.store({
96
155
  actorId: "support-bot-42",
@@ -106,18 +165,21 @@ const memories = await memstack.memory.retrieve({
106
165
  strategy: "hybrid",
107
166
  });
108
167
 
109
- // 3. Inject into your LLM call
168
+ // 3. Assemble an LLM-ready context
110
169
  const ctx = await memstack.memory.compileContext({
111
170
  actorId: "support-bot-42",
112
171
  maxTokens: 2000,
113
172
  });
114
173
 
115
- const llmResponse = await llm.complete({
174
+ const response = await llm.complete({
116
175
  system: `You are a support bot. Here is what you remember:\n${ctx.systemPrompt}`,
117
176
  user: "The user is back and still can't log in. What do you do?",
118
177
  });
119
178
 
120
- // 4. Every 100 interactions, summarization kicks in automatically.
179
+ console.log(response.text);
180
+ // "Based on our history, the user has been experiencing 503 errors on Chrome 125..."
181
+
182
+ // 4. Every 100 interactions, summarization triggers automatically.
121
183
  // Old interactions are compressed into a paragraph. Token costs stay flat.
122
184
  ```
123
185
 
@@ -192,7 +254,7 @@ No embedding adapter? `semantic` and `hybrid` fall back to keyword matching + im
192
254
 
193
255
  ### 3. Compile Context
194
256
 
195
- The killer feature. `compileContext()` takes the 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.
257
+ `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.
196
258
 
197
259
  ```typescript
198
260
  const ctx = await ms.memory.compileContext({
@@ -213,13 +275,16 @@ const ctx = await ms.memory.compileContext({
213
275
  console.log(ctx.tokenEstimate); // ~280
214
276
 
215
277
  // Inject into your LLM call
278
+ const currentMessage = "The user is asking about their refund status.";
216
279
  const response = await llm.complete({
217
280
  system: ctx.systemPrompt,
218
- user: userMessage,
281
+ user: currentMessage,
219
282
  });
283
+
284
+ console.log(response.text);
220
285
  ```
221
286
 
222
- `compileContext()` is the difference between "we have a vector DB" and "we have agent memory." It handles deduplication, token budgeting, and the recent-vs-important split that makes context useful.
287
+ `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.
223
288
 
224
289
  ### 4. Summarize
225
290
 
@@ -309,13 +374,29 @@ const ms = new MemStack({
309
374
  ### Support Agent
310
375
 
311
376
  ```typescript
377
+ // detectUrgency and classifyIntent are your own business logic.
378
+ // They could be simple keyword matchers, regex, or an LLM call.
379
+ function detectUrgency(msg: string): number {
380
+ if (msg.match(/urgent|asap|immediately/i)) return 0.9;
381
+ if (msg.match(/error|fail|broken/i)) return 0.7;
382
+ return 0.5;
383
+ }
384
+
385
+ function classifyIntent(msg: string): string[] {
386
+ const tags: string[] = [];
387
+ if (msg.match(/bill|refund|charge|payment/i)) tags.push("billing");
388
+ if (msg.match(/error|bug|fail|crash/i)) tags.push("bug");
389
+ if (msg.match(/login|password|account/i)) tags.push("account");
390
+ return tags;
391
+ }
392
+
312
393
  // Every customer message becomes a memory
313
394
  async function handleMessage(customerId: string, message: string) {
314
395
  await ms.memory.store({
315
396
  actorId: `customer:${customerId}`,
316
397
  content: message,
317
- importance: detectUrgency(message), // NLP heuristic or LLM call
318
- tags: classifyIntent(message), // "billing", "bug", "account", etc.
398
+ importance: detectUrgency(message),
399
+ tags: classifyIntent(message),
319
400
  });
320
401
 
321
402
  // Retrieve everything relevant to this customer's history
@@ -339,6 +420,12 @@ async function handleMessage(customerId: string, message: string) {
339
420
  ### RAG Pipeline
340
421
 
341
422
  ```typescript
423
+ // Suppose you have documents from your knowledge base
424
+ const documents = [
425
+ { text: "Authentication uses JWT tokens with 15-minute expiry.", url: "/docs/auth", section: "security" },
426
+ { text: "Refunds are processed within 5-10 business days.", url: "/docs/billing", section: "billing" },
427
+ ];
428
+
342
429
  // Index documents as observation memories
343
430
  for (const doc of documents) {
344
431
  await ms.memory.store({
@@ -441,9 +528,61 @@ await ms.memory.retrieve({ actorId: "x", query: "login bug", strategy: "hybrid"
441
528
 
442
529
  Embeddings power semantic search. They're optional — without them, retrieval uses keyword matching.
443
530
 
444
- **With embeddings** (configure an `EmbeddingProvider`): each `store()` computes a vector. `retrieve()` with `"semantic"` or `"hybrid"` uses cosine similarity ranking.
531
+ ### With embeddings vs Without embeddings
445
532
 
446
- **Without embeddings**: everything still works — retrieval falls back to importance + recency + keyword filters. No API costs, no setup.
533
+ **With embeddings** (`embedding` adapter configured):
534
+
535
+ ```typescript
536
+ import { MemStack, OpenAILLMAdapter, OpenAIEmbeddingAdapter, InMemoryStorageAdapter } from "@memstack/core";
537
+
538
+ const ms = new MemStack({
539
+ llm: new OpenAILLMAdapter({ apiKey: process.env.OPENAI_API_KEY! }),
540
+ embedding: new OpenAIEmbeddingAdapter({ apiKey: process.env.OPENAI_API_KEY! }),
541
+ storage: new InMemoryStorageAdapter(),
542
+ });
543
+
544
+ // store() computes a 1536-dim vector automatically
545
+ await ms.memory.store({
546
+ actorId: "agent-7",
547
+ content: "Customer asked about refund policy for Q2 purchases.",
548
+ });
549
+
550
+ // retrieve() with "semantic" or "hybrid" uses cosine similarity
551
+ // Query: "refund" finds the refund policy memory even though the word "refund"
552
+ // appears differently across stored memories.
553
+ const results = await ms.memory.retrieve({
554
+ actorId: "agent-7",
555
+ query: "how do I get my money back",
556
+ strategy: "semantic",
557
+ });
558
+ // Matches "Customer asked about refund policy" — semantic match, not keyword match.
559
+ ```
560
+
561
+ **Without embeddings** (no `embedding` adapter):
562
+
563
+ ```typescript
564
+ const ms = new MemStack({
565
+ llm: new OpenAILLMAdapter({ apiKey: process.env.OPENAI_API_KEY! }),
566
+ storage: new InMemoryStorageAdapter(),
567
+ // no embedding adapter
568
+ });
569
+
570
+ // store() works identically, just no vector computed
571
+ await ms.memory.store({
572
+ actorId: "agent-7",
573
+ content: "Customer asked about refund policy for Q2 purchases.",
574
+ });
575
+
576
+ // retrieve() with "semantic" or "hybrid" falls back to keyword matching
577
+ // plus importance/recency sorting. No API costs, no setup required.
578
+ const results = await ms.memory.retrieve({
579
+ actorId: "agent-7",
580
+ query: "refund",
581
+ strategy: "hybrid", // falls back to keyword + importance
582
+ });
583
+ // Still works — finds "refund" via substring match. Less precise for
584
+ // paraphrased queries ("money back" won't match "refund").
585
+ ```
447
586
 
448
587
  **Batch embedding:** `storeBatch()` sends all texts in one embedding API call, reducing cost and latency.
449
588
 
@@ -456,6 +595,28 @@ const ms = new MemStack({
456
595
  });
457
596
  ```
458
597
 
598
+ ### Vector dimensions and model compatibility
599
+
600
+ 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.
601
+
602
+ | Adapter | Default model | Dimensions |
603
+ |---------|--------------|------------|
604
+ | `OpenAIEmbeddingAdapter` | `text-embedding-3-small` | 1536 |
605
+ | `OpenAIEmbeddingAdapter` | `text-embedding-3-large` | 3072 |
606
+ | `CohereEmbeddingAdapter` | `embed-english-v3.0` | 1024 |
607
+ | `CohereEmbeddingAdapter` | `embed-english-light-v3.0` | 384 |
608
+ | `CohereEmbeddingAdapter` | `embed-english-v2.0` | 4096 |
609
+ | `CohereEmbeddingAdapter` | `embed-multilingual-v3.0` | 1024 |
610
+
611
+ **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.
612
+
613
+ **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.
614
+
615
+ **DeepSeek users:** DeepSeek has no embeddings API. If you use DeepSeek as your LLM, you must either:
616
+ 1. Omit the embedding adapter and use `"recent"` or `"important"` retrieval strategies (no API costs, less precise)
617
+ 2. Pair DeepSeek with a separate embedding provider (e.g., OpenAI for embeddings, DeepSeek for chat)
618
+
619
+
459
620
  ---
460
621
 
461
622
  ## Adapters
@@ -512,6 +673,8 @@ new OpenAIEmbeddingAdapter({ apiKey: "...", baseURL: "https://api.voyageai.com/v
512
673
 
513
674
  MemStack ships with **11 production-ready storage adapters (7 experimental)** — every major backend, zero peer dependencies, all client-injected.
514
675
 
676
+ ### Production (e2e verified against real instances)
677
+
515
678
  **Built-in (zero external deps):**
516
679
  | Adapter | Backend | Use case |
517
680
  |---|---|---|
@@ -524,21 +687,11 @@ MemStack ships with **11 production-ready storage adapters (7 experimental)**
524
687
  | Adapter | Backend | Vector search |
525
688
  |---|---|---|
526
689
  | `PostgresStorageAdapter` | PostgreSQL + pgvector | HNSW native |
527
- | `SQLiteStorageAdapter` | SQLite (better-sqlite3) | In-memory cosine |
528
- | `TursoStorageAdapter` | Turso (libsql) | DiskANN native |
529
-
530
- **Aggregators:**
531
- | Adapter | Delegates to |
532
- |---|---|
533
- | `Mem0StorageAdapter` | Mem0 OSS or Cloud |
534
- | `ZepStorageAdapter` | Zep Cloud or Community Edition |
535
690
 
536
691
  **Vector databases:**
537
692
  | Adapter | Backend |
538
693
  |---|---|
539
694
  | `QdrantStorageAdapter` | Qdrant |
540
- | `PineconeStorageAdapter` | Pinecone |
541
- | `ChromaStorageAdapter` | ChromaDB |
542
695
  | `WeaviateStorageAdapter` | Weaviate |
543
696
  | `LanceDBStorageAdapter` | LanceDB |
544
697
  | `MongoDBStorageAdapter` | MongoDB Atlas Vector Search |
@@ -547,13 +700,28 @@ MemStack ships with **11 production-ready storage adapters (7 experimental)**
547
700
  | Adapter | Backend |
548
701
  |---|---|
549
702
  | `RedisStorageAdapter` | Redis (ioredis) |
550
- | `UpstashStorageAdapter` | Upstash Redis + Vector |
551
703
 
552
704
  **Graph:**
553
705
  | Adapter | Backend |
554
706
  |---|---|
555
707
  | `Neo4jStorageAdapter` | Neo4j |
556
708
 
709
+ ### Experimental (mock-tested, blocked by cloud deps or platform constraints)
710
+
711
+ Available via direct source import. Not yet in the barrel export — uncomment in `src/index.ts` when e2e verified.
712
+
713
+ | Adapter | Backend | Blocker |
714
+ |---|---|---|
715
+ | `SQLiteStorageAdapter` | SQLite (better-sqlite3) | Native binary for Node 24 |
716
+ | `TursoStorageAdapter` | Turso (libsql) | Cloud-only (needs Turso account) |
717
+ | `ChromaStorageAdapter` | ChromaDB | Embedding function dependency |
718
+ | `PineconeStorageAdapter` | Pinecone | Cloud-only (needs API key) |
719
+ | `UpstashStorageAdapter` | Upstash Redis + Vector | Cloud-only (needs API key) |
720
+ | `Mem0StorageAdapter` | Mem0 OSS or Cloud | Cloud-only (needs API key) |
721
+ | `ZepStorageAdapter` | Zep Cloud or CE | Cloud-only (needs API key) |
722
+
723
+ > **Direct import:** `import { ChromaStorageAdapter } from "@memstack/core/src/adapters/storage/chroma.js"`
724
+
557
725
  **Quick-start per backend:**
558
726
 
559
727
  ```ts
@@ -561,11 +729,6 @@ MemStack ships with **11 production-ready storage adapters (7 experimental)**
561
729
  import { PostgresStorageAdapter } from "@memstack/core";
562
730
  const storage = new PostgresStorageAdapter({ connectionString: "postgres://..." });
563
731
 
564
- // SQLite
565
- import Database from "better-sqlite3";
566
- import { SQLiteStorageAdapter } from "@memstack/core";
567
- const storage = new SQLiteStorageAdapter({ db: new Database("memory.db") });
568
-
569
732
  // Redis
570
733
  import Redis from "ioredis";
571
734
  import { RedisStorageAdapter } from "@memstack/core";
@@ -604,26 +767,21 @@ class MyStorage implements StorageProvider {
604
767
 
605
768
  ## Backend Comparison
606
769
 
607
- | Backend | Vector search | Touch | Best for |
770
+ | Backend | Vector search | Touch | Status |
608
771
  |---|---|---|---|
609
- | InMemory | Cosine in-memory | Yes | Testing, prototyping |
610
- | Disk (JSON) | Keyword + importance | Yes | Simple local persistence |
611
- | Markdown | Keyword + importance | No | Human-readable, git-diffable |
612
- | Postgres | pgvector HNSW | Yes | Production relational |
613
- | SQLite | Cosine in-memory | Yes | Local dev, solo apps |
614
- | Turso | DiskANN native | Yes | Edge/serverless |
615
- | Redis | RediSearch KNN (auto-detect) | Yes | Sub-5ms hot session state |
616
- | Upstash | Native vector (vector mode) | No | CF Workers, Vercel Edge |
617
- | Qdrant | ANN native | No | Best filtered search |
618
- | Pinecone | ANN native | No | Zero-ops managed |
619
- | Chroma | Native | No | LangChain prototyping |
620
- | Weaviate | BM25 + vector hybrid | No | Hybrid search |
621
- | LanceDB | DiskANN native | No | Embedded local vector |
622
- | MongoDB | Atlas Vector Search | No | Existing MongoDB deployments |
623
- | Neo4j | Neo4j vector index | No | Relationship-aware agents |
624
- | Hybrid | Delegates to cache/durable | If durable supports | Read-through cache pattern |
625
- | Mem0 | Delegates to Mem0 | No | Multi-backend via Mem0 |
626
- | Zep | Graphiti temporal graph | No | Temporal graph memory |
772
+ | InMemory | Cosine in-memory | Yes | ✅ Production |
773
+ | Disk (JSON) | Keyword + importance | Yes | ✅ Production |
774
+ | Markdown | Keyword + importance | No | ✅ Production |
775
+ | Postgres | pgvector HNSW | Yes | ✅ Production |
776
+ | Redis | RediSearch KNN (auto-detect) | Yes | ✅ Production |
777
+ | Qdrant | ANN native | No | ✅ Production |
778
+ | Weaviate | BM25 + vector hybrid | No | ✅ Production |
779
+ | LanceDB | DiskANN native | No | ✅ Production |
780
+ | MongoDB | Atlas Vector Search | No | ✅ Production |
781
+ | Neo4j | Neo4j vector index | No | ✅ Production |
782
+ | Hybrid | Delegates to cache/durable | If durable supports | ✅ Production |
783
+ | SQLite | Cosine in-memory | Yes | ✅ Production |
784
+ | Hybrid | Delegates to cache/durable | If durable supports | ✅ Production |
627
785
 
628
786
  ---
629
787
 
@@ -676,7 +834,11 @@ ms.memory.dryRunPrune(strategy: PruneStrategy): Promise<{ wouldPrune: string[];
676
834
  ms.memory.count(filter?: MemoryCountFilter): Promise<number>
677
835
  ms.memory.delete(id: string): Promise<void>
678
836
  ms.memory.deleteMany(ids: string[]): Promise<number>
679
- ms.memory.touch(id: string): Promise<void> // bump recency without changing content
837
+ ms.memory.touch(id: string): Promise<void>
838
+ ms.memory.purgeActor(actorId: string): Promise<number>
839
+ ms.memory.merge(ids: string[]): Promise<Memory>
840
+ ms.memory.stats(actorId?: string): Promise<MemoryStats>
841
+ ms.memory.summarizeStream(options: SummarizeOptions): AsyncIterable<{ chunk: string; text: string }>
680
842
  ```
681
843
 
682
844
  ### Export / Import
@@ -684,6 +846,8 @@ ms.memory.touch(id: string): Promise<void> // bump recency without changing con
684
846
  Snapshot and restore full state for persistence, backups, or migration:
685
847
 
686
848
  ```typescript
849
+ import * as fs from "node:fs";
850
+
687
851
  // Save
688
852
  const snapshot = await ms.export();
689
853
  fs.writeFileSync("state.json", JSON.stringify(snapshot, null, 2));
@@ -785,6 +949,7 @@ cd memstack
785
949
  pnpm install
786
950
 
787
951
  pnpm test # 393 tests, no external services needed
952
+ pnpm test:e2e # 82 E2E tests (requires Docker)
788
953
  pnpm test:watch # Watch mode
789
954
  pnpm build # CJS + ESM + type declarations
790
955
  pnpm check # TypeScript type-check only
@@ -839,7 +1004,7 @@ npm login
839
1004
  npm publish --access public
840
1005
  ```
841
1006
 
842
- The `@memstack` scope requires `--access public` on first publish.
1007
+ The `@memstack` scope requires `--access public`.
843
1008
 
844
1009
  ---
845
1010
 
@@ -847,7 +1012,6 @@ The `@memstack` scope requires `--access public` on first publish.
847
1012
 
848
1013
  Most needed contributions:
849
1014
 
850
- - **Docker Compose** for integration testing
851
1015
  - **LLM adapters**: Google Gemini (native), Amazon Bedrock, Vertex AI
852
1016
  - **Embedding adapters**: local inference (transformers.js, ONNX)
853
1017
  - **Benchmarks**: retrieval quality, latency, cost comparisons