Memory Package
The @hazeljs/memory package is a pluggable user memory layer with multi-store support. It provides a single MemoryStore interface, multiple backends (in-memory by default, optional Postgres, Redis, Prisma, vector), and a store-agnostic MemoryService for building profile, preference, behavioral, emotional, episodic, and semantic memory—so RAG and agents can share one memory model.
What problem it solves
AI apps that personalize responses, remember context, or coordinate RAG with agents run into the same issues:
- Scattered memory — Conversation history lives in RAG, preferences in env or a separate DB, and agents have no shared view. You end up with duplicate logic and no single source of truth.
- Wrong storage for the job — Episodic recall (e.g. “what did we discuss?”) needs semantic search; preferences need durable key-value; emotional state (frustrated, excited) should expire. One rigid store does not fit all.
- Tight coupling — If memory is hard-wired to one database, switching to Postgres or adding a vector store means rewriting your memory layer instead of swapping a backend.
@hazeljs/memory addresses this by:
- One model, one API — A single
MemoryItemschema (userId, category, key, value, confidence, source, evidence, timestamps, optional TTL) and a singleMemoryServiceAPI. RAG and agents can both use it via the same store. - Categories that match how you think — Profile, preference, behavioral, emotional, episodic, semantic summary. So you can store “user prefers dark mode” as preference, “user is frustrated right now” as emotional (with TTL), and “last Tuesday we discussed deployment” as episodic.
- Explicit vs inferred — Every item has
source(explicit, inferred, system) andconfidence. You can enforce “explicit over inferred” on updates and let the system learn from behavior without overwriting what the user said. - Pluggable stores — In-memory by default (zero infra). When you need durability, scale, or semantic search, plug in Postgres, Redis, Prisma, or a vector store without changing application code—only the store you pass to
MemoryServicechanges. - Optional composite — Route by category: e.g. primary store (Postgres or Redis) for most data, and a vector-backed store for episodic/semantic so “find similar past conversations” stays fast and accurate.
So: one memory layer that fits preferences, behavior, emotional state, and episodic context, with a default that works out of the box and optional stores for production.
Purpose
- In-memory by default — No database or env vars required; add optional Postgres, Redis, Prisma, or vector when you need durability or semantic recall.
- Memory categories — Profile, preference, behavioral, emotional, episodic, semantic summary.
- Explicit vs inferred — Each item has
source(explicit, inferred, system),confidence, andevidence. - TTL for emotional — Short-lived emotional state (frustrated, excited) with configurable expiry.
- Multi-store — Same interface for InMemoryStore, PostgresStore, RedisStore, PrismaMemoryStore, VectorEpisodicStore; optional CompositeMemoryStore to route by category.
- Store-agnostic API —
MemoryServicefor save, query, get, update, delete, getStats, prune, optional search.
Key Features
| Feature | Description |
|---|---|
| Default store | createDefaultMemoryStore() — in-memory, zero config |
| Store interface | Single MemoryStore; all backends implement it |
| Categories | profile, preference, behavioral, emotional, episodic, semantic_summary |
| Unified item | id, userId, category, key, value, confidence, source, evidence, createdAt, updatedAt, expiresAt, accessCount, sessionId |
| Optional adapters | Postgres, Redis, Vector (episodic/semantic); optional peer deps |
| Composite store | Route by category to primary + optional episodic (vector) store |
| Factory | createMemoryStore(config) for type: 'in-memory' | 'postgres' | 'composite' |
Architecture
flowchart LR subgraph app [App] MS[MemoryService] end subgraph stores [Stores] IM[InMemoryStore] PG[PostgresStore] RD[RedisStore] VEC[VectorEpisodicStore] COMP[CompositeStore] end MS -->|default| IM MS -->|optional| PG MS -->|optional| RD MS -->|optional| COMP COMP --> PG COMP --> VEC
- MemoryService — High-level API: save, get, query, update, delete, getByUserAndCategory, search (if store supports it), getStats, prune. Enforces explicit-over-inferred and applies TTL for emotional when configured.
- Stores — All implement the same
MemoryStoreinterface. Default is InMemoryStore; add adapters for Postgres, Redis, or vector episodic/semantic. - Composite store — Optional: primary store for most categories, episodic (e.g. vector) store for episodic/semantic_summary.
Installation
No database required for in-memory mode:
npm install @hazeljs/memory
For optional backends (peer/optional deps, install only what you use):
# PostgreSQL (e.g. pg pool)
npm install @hazeljs/memory pg
# Redis
npm install @hazeljs/memory ioredis
# Prisma (if using the Prisma adapter from the package)
npm install @hazeljs/memory @prisma/client
Quick Start
Same API for every store: create a store, pass it to MemoryService, then save/query:
import { createDefaultMemoryStore, MemoryService } from '@hazeljs/memory';
const store = createDefaultMemoryStore();
const memoryService = new MemoryService(store);
await memoryService.initialize();
await memoryService.save({
userId: 'user-1',
category: 'preference',
key: 'theme',
value: 'dark',
confidence: 1,
source: 'explicit',
evidence: [],
});
const items = await memoryService.query({
userId: 'user-1',
category: 'preference',
limit: 10,
});
Below are examples for every store type so you can choose the right backend.
Stores (with examples)
All stores implement the same MemoryStore interface. Your app uses MemoryService(store); only the store instance changes.
1. In-memory (default)
No database, no env vars. Data is lost on restart. Best for development, tests, or single-process apps that don't need durability.
import { createDefaultMemoryStore, MemoryService } from '@hazeljs/memory';
const store = createDefaultMemoryStore({
maxTotalItems: 100_000,
maxItemsPerUserPerCategory: 5000,
});
const memoryService = new MemoryService(store);
await memoryService.initialize();
2. Postgres (raw pg pool)
Durable, queryable, good for production when you already use PostgreSQL. You provide a pool with a query(sql, params) method (e.g. from pg). The store creates the table and indexes on first initialize() if you use the built-in schema.
import { Pool } from 'pg';
import { PostgresStore, MemoryService } from '@hazeljs/memory';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const store = new PostgresStore({
pool: {
query: (sql: string, params?: unknown[]) => pool.query(sql, params),
},
tableName: 'memory_items', // optional, default: memory_items
});
const memoryService = new MemoryService(store);
await memoryService.initialize();
3. Redis
Fast, good for session-scoped or short-TTL memory (e.g. emotional state). You provide an ioredis-style client (get, set, del, sadd, smembers, mget, keys). Optional key prefix and default TTL for expired items.
import Redis from 'ioredis';
import { RedisStore, MemoryService } from '@hazeljs/memory';
const client = new Redis(process.env.REDIS_URL!);
const store = new RedisStore({
client: {
get: (k) => client.get(k),
set: (k, v, ...args) => client.set(k, v, ...args),
del: (...keys) => client.del(...keys),
sadd: (k, ...m) => client.sadd(k, ...m),
smembers: (k) => client.smembers(k),
srem: (k, ...m) => client.srem(k, ...m),
mget: (...keys) => client.mget(...keys),
keys: (p) => client.keys(p),
},
keyPrefix: 'memory',
defaultTtlSeconds: 30 * 60, // for emotional/expiresAt
});
const memoryService = new MemoryService(store);
await memoryService.initialize();
4. Prisma
Durable storage using your app’s Prisma client (or the one generated from @hazeljs/memory’s schema). Same pattern as @hazeljs/flow and flow-runtime: one schema, one client, full type safety.
import { createPrismaMemoryStore, getMemoryPrismaClient } from '@hazeljs/memory/prisma';
import { MemoryService } from '@hazeljs/memory';
const prisma = getMemoryPrismaClient(process.env.DATABASE_URL);
const store = createPrismaMemoryStore(prisma);
const memoryService = new MemoryService(store);
await memoryService.initialize();
Run Prisma migrations from the memory package (or copy the MemoryItem model into your schema) and prisma generate before build.
5. Vector episodic (semantic recall)
For episodic and semantic_summary categories when you need similarity search (e.g. “find past conversations like this”). Items are held in memory; an optional VectorStoreAdapter is used for search. If you omit the vector store, search() returns empty; query by userId/category still works.
import { VectorEpisodicStore, MemoryService, MemoryCategory } from '@hazeljs/memory';
const store = new VectorEpisodicStore({
categories: [MemoryCategory.EPISODIC, MemoryCategory.SEMANTIC_SUMMARY],
vectorStore: myVectorAdapter, // { add(id, embedding, metadata), search(embedding, { topK, filter }), delete(id) }
});
const memoryService = new MemoryService(store);
await memoryService.initialize();
6. Composite (primary + episodic)
Route by category: primary store (e.g. Postgres or Redis) for profile, preference, behavioral, emotional; optional episodic store (e.g. VectorEpisodicStore) for episodic and semantic_summary. One MemoryService talks to the composite; the composite delegates to the right store per category.
import {
createDefaultMemoryStore,
CompositeMemoryStore,
VectorEpisodicStore,
MemoryCategory,
MemoryService,
} from '@hazeljs/memory';
const primary = createDefaultMemoryStore();
const episodic = new VectorEpisodicStore({
categories: [MemoryCategory.EPISODIC, MemoryCategory.SEMANTIC_SUMMARY],
vectorStore: myVectorAdapter,
});
const composite = new CompositeMemoryStore({ primary, episodic });
const memoryService = new MemoryService(composite);
await memoryService.initialize();
7. Factory: createMemoryStore(config)
Create a store from config without importing each adapter. Useful for env-driven or config-file-driven setup.
import { createMemoryStore, MemoryService } from '@hazeljs/memory';
const store = createMemoryStore({
type: 'in-memory',
options: { maxTotalItems: 50_000 },
});
// or type: 'postgres', options: { pool }; or type: 'redis', options: { client }; or type: 'composite', options: { primary, episodic }
const memoryService = new MemoryService(store);
await memoryService.initialize();
When to use which store
| Store | Best for | Durability | Setup |
|---|---|---|---|
| In-memory | Development, tests, single-process apps | Lost on restart | None |
| Postgres | Production, analytics, complex queries | Durable | pg pool + table |
| Redis | Session cache, emotional TTL, high read/write | Configurable (AOF/RDB) | Redis client |
| Prisma | Apps already using Prisma, type-safe schema | Durable | Prisma + migrations |
| Vector episodic | Semantic search over past conversations/events | In-memory only unless you persist the vector index | Optional vector adapter |
| Composite | Primary (e.g. Postgres) + vector for episodic/semantic | Depends on primary + episodic | Primary + episodic stores |
Memory categories
| Category | Typical use |
|---|---|
| profile | User profile attributes |
| preference | Likes, dislikes, taste (long-term) |
| behavioral | Recurring habits, patterns (inferred) |
| emotional | Short-term state (frustrated, excited); use TTL |
| episodic | Past conversations, events |
| semantic_summary | Compressed summaries of interactions |
Integration with RAG and Agent
RAG and Agent can use the same memory backend:
- RAG — Use the HazelMemoryStoreAdapter from
@hazeljs/rag/memory-hazel: it implements RAG'sMemoryStoreand delegates to@hazeljs/memory'sMemoryService. Pass the sameMemoryManager(backed by this adapter) toRAGPipelineWithMemoryand to everyAgentRuntimefor in-process shared memory. - Agent — No code change. Agent already accepts
memoryManager?: MemoryManagerfrom RAG. When you use the adapter, both RAG and agents share one store and oneMemoryManagerin the same Node process.
See the Memory System guide for the full shared memory (RAG + Agent) pattern and the RAG Package for createHazelMemoryStoreAdapter and @hazeljs/rag/memory-hazel.
When to use it
Good fit: User preferences, profile and behavioral memory, emotional state (with TTL), episodic conversation context, and any app that wants one memory model shared by RAG and agents with pluggable storage (in-memory, Postgres, Redis, vector).
Less ideal: Simple one-off caching (use Cache), or when you only need RAG's built-in BufferMemory/VectorMemory and don't need the unified categories or multi-store.
What's next?
- Memory System — RAG + Agent shared memory, in-process pattern, and using @hazeljs/memory as the backend
- RAG Package — MemoryManager, RAGPipelineWithMemory, and the HazelMemoryStoreAdapter entry point (
@hazeljs/rag/memory-hazel) - Agent Package — AgentRuntime and memoryManager config