Persistent memory layer for TypeScript AI agents — semantic vector search plus SQLite-backed rows in one API.
agent-memory-store is a hybrid memory layer for TypeScript agents:
- Vector similarity (semantic recall) via ChromaDB or an in-memory backend for tests.
- Structured storage via libSQL (SQLite-compatible file) for transactional, filterable records.
Most chat agents forget context after each session. This library keeps episodic, semantic, and procedural memories on disk, with hybrid retrieval (meaningful search plus SQL-style filters on metadata, time, and type).
| Feature | Description |
|---|---|
| Hybrid storage | Embeddings in Chroma (or memory) + rows in SQLite |
| Persistent SQL | Memories survive restarts (MEMORY_SQLITE_PATH) |
| Memory types | episodic, semantic, procedural |
| Embedders | OpenAI, Ollama, Transformers.js (optional), or a custom function |
| Time-aware queries | where + orderBy + limit (structured filters over stored rows) |
| Consolidation | Pluggable rules to merge or forget memories |
| REST API | Optional HTTP server on port 3000 (configurable) |
| Snapshots | createSnapshot / restoreSnapshot for SQLite + vector export |
- Node.js 22+
- For semantic search in production, run a Chroma server (for example Docker on port 8000) and set
MEMORY_VECTOR_BACKEND=chromaandCHROMA_URL. - For OpenAI embeddings, set
OPENAI_API_KEY. Without it, the bundled REST server falls back to a deterministic hash embedder (fine for demos; not semantically meaningful).
cd agent-memory-store
npm install
npm run build| Script | Description |
|---|---|
npm run build |
Compile TypeScript → dist/ |
npm run dev |
REST API with hot reload (tsx) |
npm start |
REST API from dist/ (after build) |
npm test |
Unit tests |
npm run verify |
build + test (CI-friendly) |
npm run test:integration |
Chroma HTTP test (skipped if no server) |
npm run test:performance |
One recall latency smoke test |
npm run test:watch |
Vitest watch mode |
Copy .env.example to .env.local and adjust:
| Variable | Purpose |
|---|---|
OPENAI_API_KEY |
OpenAI embeddings when embedder: 'openai' |
OLLAMA_BASE_URL |
Default http://127.0.0.1:11434 |
OLLAMA_EMBED_MODEL |
e.g. nomic-embed-text |
MEMORY_SQLITE_PATH |
SQLite file (default ./data/memory.db) |
MEMORY_CHROMA_PATH |
Used to derive the Chroma collection name suffix when MEMORY_VECTOR_BACKEND=chroma (not a filesystem path for the HTTP client) |
CHROMA_URL |
Chroma HTTP API (default http://127.0.0.1:8000) |
MEMORY_VECTOR_BACKEND |
memory (default) or chroma |
MEMORY_SNAPSHOT_DIR |
Snapshot directory (default ./snapshots) |
PORT |
REST API port (default 3000) |
Library-only options (constructor): openaiApiKey, openaiModel, ollamaBaseUrl, ollamaModel (override env for named embedders); chromaUrl, chromaPath or collectionSuffix (Chroma collection naming); embeddingCacheSize, snapshotDir.
Vector backends
memory— in-process vectors; fastest for unit tests. Vectors are not persisted (SQL rows are still persisted).chroma— persistent vectors through a Chroma HTTP service.
import { MemoryStore } from "agent-memory-store";
import { createHashEmbedder } from "agent-memory-store"; // local dev / tests
const store = new MemoryStore({
embedder: createHashEmbedder(64), // or 'openai' | 'ollama' | 'transformers' | async (t) => [...]
sqlitePath: "./data/memory.db",
vectorBackend: "memory",
});
await store.init();
const id = await store.remember({
content: "User prefers concise answers with bullet points",
type: "semantic",
metadata: { topic: "preferences", importance: 0.9 },
});
const similar = await store.recall({
query: "How should I format responses?",
limit: 3,
});
const important = await store.query({
where: {
type: "semantic",
"metadata.importance": { $gte: 0.8 },
},
orderBy: { timestamp: "desc" },
limit: 5,
});npm run dev
# or: npm run build && npm startEndpoints (JSON):
| Method | Path | Description |
|---|---|---|
GET |
/health |
Liveness |
POST |
/api/memories |
Body: { content, type, metadata?, id?, timestamp? } → { id } (content string; type: episodic, semantic, or procedural) |
POST |
/api/recall |
Body: { query, limit?, type?, minSimilarity? } (query non-empty string) |
POST |
/api/query |
Body: { where?, orderBy?, limit? } |
DELETE |
/api/memories/:id |
Delete one memory |
POST |
/api/consolidate |
Body: { dryRun?: boolean } → { mergedPairs } (no-op unless rules were set via setConsolidationRules in code) |
GET |
/api/working-memory?limit= |
Recent episodic memories |
POST |
/api/snapshots |
Create snapshot → { id } |
POST |
/api/snapshots/:id/restore |
Restore snapshot |
┌─────────────────────────────────────────────────────────────┐
│ MemoryStore │
│ remember() recall() query() forget() consolidate() │
└─────────────────────────────┬───────────────────────────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌────────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Vector backend │ │ LibSQL │ │ Embedding cache │
│ Chroma / RAM │ │ (SQLite file)│ │ (LRU) │
└────────────────┘ └──────────────┘ └─────────────────┘
Creates the SQLite schema if needed. Call once before other methods.
Closes the libSQL connection (call when shutting down a long-running process).
Stores a memory; returns id. Embeddings are generated automatically unless embedding is provided.
type must be one of episodic, semantic, or procedural (validated at runtime so bad JSON cannot corrupt the store).
content must be a string. timestamp may be a Date or an ISO string (typical for JSON bodies from the REST API).
Semantic search. query must be a non-empty string (whitespace-only is rejected). minSimilarity is applied after converting Chroma distance to a 0–1 style score (see implementation). Tune for your embedding model.
Structured filters over stored rows, including:
type,timestamp(with$gte,$lte, …) — timestamp bounds may beDate, epoch ms, or ISO strings (as in JSON over REST),metadata.someKeyfor nested metadata (dot path).
Deletes from SQL and the vector index.
Runs setConsolidationRules handlers. Returns the number of merge operations performed (forget rules run but are not counted).
Latest episodic memories (by timestamp). Invalid or non-positive limit values fall back to 20.
Copies the SQLite file and exports vectors to MEMORY_SNAPSHOT_DIR/<id>/, then can restore both stores.
Provide merge rules (condition + merge) or forget rules (condition + action: 'forget').
Optional helpers (same package):
defaultMergeCondition(a, b, minSim?)— same-type semantic memories with cosine similarity ≥minSim.defaultMergeFn(a, b)— concatenates content and merges metadata.
MEMORY_SQLITE_PATH may be a plain path (./data/memory.db) or a libSQL-style file: URL (file:./data/memory.db). Directory creation and snapshot file copies use a normalized filesystem path automatically.
A sample docker-compose.yml is included (Chroma + API). Build the image after npm run build:
npm run build
docker compose build
docker compose up -dRequires Docker; set OPENAI_API_KEY for real embeddings or rely on the server’s hash embedder for smoke tests only.
See the Scripts table for npm test, npm run verify, npm run test:performance, and npm run test:watch.
Chroma: tests/chroma.integration.test.ts is excluded from default npm test. Run npm run test:integration to probe CHROMA_URL first — if no server responds, the suite is skipped (exit 0); with Chroma running, the test executes.
agent-memory-store/
├── src/
│ ├── MemoryStore.ts
│ ├── util/ # SQLite path helpers for fs
│ ├── sql/ # LibSQL adapter + where helpers
│ ├── vector/ # Chroma + in-memory adapters
│ ├── embedding/ # OpenAI, Ollama, transformers (optional), hash
│ ├── consolidation/
│ └── api/ # Express REST + server entry
├── tests/ # `*.test.ts` + optional `chroma.integration.test.ts`
├── vitest.config.ts
├── docker-compose.yml
├── Dockerfile
├── .env.example
├── package.json
├── LICENSE
└── tsconfig.json
MIT — see LICENSE. Helpers isMemoryType and assertMemoryType are exported for callers validating payloads.