Memory Package

npm downloads

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:

  1. One model, one API — A single MemoryItem schema (userId, category, key, value, confidence, source, evidence, timestamps, optional TTL) and a single MemoryService API. RAG and agents can both use it via the same store.
  2. 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.
  3. Explicit vs inferred — Every item has source (explicit, inferred, system) and confidence. You can enforce “explicit over inferred” on updates and let the system learn from behavior without overwriting what the user said.
  4. 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 MemoryService changes.
  5. 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, and evidence.
  • 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 APIMemoryService for save, query, get, update, delete, getStats, prune, optional search.

Key Features

FeatureDescription
Default storecreateDefaultMemoryStore() — in-memory, zero config
Store interfaceSingle MemoryStore; all backends implement it
Categoriesprofile, preference, behavioral, emotional, episodic, semantic_summary
Unified itemid, userId, category, key, value, confidence, source, evidence, createdAt, updatedAt, expiresAt, accessCount, sessionId
Optional adaptersPostgres, Redis, Vector (episodic/semantic); optional peer deps
Composite storeRoute by category to primary + optional episodic (vector) store
FactorycreateMemoryStore(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 MemoryStore interface. 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

StoreBest forDurabilitySetup
In-memoryDevelopment, tests, single-process appsLost on restartNone
PostgresProduction, analytics, complex queriesDurablepg pool + table
RedisSession cache, emotional TTL, high read/writeConfigurable (AOF/RDB)Redis client
PrismaApps already using Prisma, type-safe schemaDurablePrisma + migrations
Vector episodicSemantic search over past conversations/eventsIn-memory only unless you persist the vector indexOptional vector adapter
CompositePrimary (e.g. Postgres) + vector for episodic/semanticDepends on primary + episodicPrimary + episodic stores

Memory categories

CategoryTypical use
profileUser profile attributes
preferenceLikes, dislikes, taste (long-term)
behavioralRecurring habits, patterns (inferred)
emotionalShort-term state (frustrated, excited); use TTL
episodicPast conversations, events
semantic_summaryCompressed 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's MemoryStore and delegates to @hazeljs/memory's MemoryService. Pass the same MemoryManager (backed by this adapter) to RAGPipelineWithMemory and to every AgentRuntime for in-process shared memory.
  • Agent — No code change. Agent already accepts memoryManager?: MemoryManager from RAG. When you use the adapter, both RAG and agents share one store and one MemoryManager in 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