@achref_hasni/membridge
v0.1.1
Published
The vendor-neutral TypeScript interface and benchmark harness for AI agent memory. Write once, swap memory backends (Mem0, Zep, Letta) with a config change, and get honest latency, cost, and recall comparisons for your LLM agents.
Maintainers
Readme
Table of contents
- What MemBridge is (and is not)
- Why MemBridge
- Quickstart
- Installation
- The contract
- Configuration
- Environment variables
- API reference
- CLI reference
- Architecture
- Writing a driver
- Testing & replay cassettes
- Benchmark (roadmap)
- Milestone status
- Development
- Troubleshooting
- Contributing
- License
What MemBridge is (and is not)
MemBridge is the missing abstraction layer for AI agent memory. The ecosystem is crowded with incompatible backends — Mem0, Zep, Letta (MemGPT) — each with its own SDK, storage model, and cost profile. MemBridge gives you one small, stable API over all of them, so your LLM agent, RAG pipeline, or chatbot is not welded to a single vendor.
| MemBridge is | MemBridge is not |
|------------------|----------------------|
| A vendor-neutral MemoryDriver interface | A vector database or embedding model |
| A driver registry + adapter pattern | A hosted memory store |
| A benchmark harness (M3–M4, planned) | A knowledge graph engine |
| An escape hatch to raw vendor SDKs | A replacement for backend-specific features |
npm package name:
@achref_hasni/membridge— the unscoped namemembridgeis blocked by npm (too similar to the existingmem-bridgepackage). The CLI command is stillmembridge.
M1 reality check: Only the Mem0 driver is implemented today. Zep and Letta are declared as optional peer dependencies and documented for M2 — swapping to them in config will fail until those drivers land.
Why MemBridge
- One interface, any backend.
add/search/get/deleteover a normalized memory model. Switch backends by editing config — zero call-site changes (once the target driver exists). - Honest benchmarks (planned). Same dataset, same machine, same workload for every driver. Reports p50 and p95 (never just averages) and cost in dollars with the pricing assumption stated.
- No lock-in, no hidden features. Every driver exposes
.raw()— the real vendor client — so a backend's signature feature is always one call away. - Built for trust. Strict TypeScript, readable source, pinned deps, and recorded cassettes in tests so driver behavior is reproducible without a live vendor account.
Quickstart
1. Install
pnpm add @achref_hasni/membridge mem0ai
# mem0ai is the Mem0 SDK — install only the backend(s) you use2. Set your API key
cp .env.example .env
# Edit .env and set MEM0_API_KEY=...Or pass the key inline in config (see Configuration).
3. Use in code
import { createMemory } from '@achref_hasni/membridge';
const memory = await createMemory({
drivers: [{ name: 'mem0', options: { apiKey: process.env.MEM0_API_KEY } }],
});
await memory.add({
userId: 'alice',
messages: [{ role: 'user', content: 'I always want a window seat' }],
});
const hits = await memory.search({ userId: 'alice', query: 'seating preference', limit: 5 });
console.log(hits[0]?.content); // "Prefers a window seat" (score: 0.91)4. Verify the CLI
pnpm build
node dist/cli/index.js list
# Registered drivers:
# - mem0Installation
Package managers
# pnpm (recommended — matches packageManager in package.json)
pnpm add @achref_hasni/membridge mem0ai
# npm
npm install @achref_hasni/membridge mem0ai
# yarn
yarn add @achref_hasni/membridge mem0aiPeer dependencies (optional backends)
MemBridge declares backend SDKs as optional peer dependencies. You only install what you use:
| Driver | npm package | Status |
|--------|-------------|--------|
| mem0 | mem0ai (>=3) | ✅ M1 |
| zep | @getzep/zep-cloud (>=3) | ⏳ M2 |
| letta | @letta-ai/letta-client (>=1) | ⏳ M2 |
If you reference a driver name that is not registered, createMemory() throws a MemBridgeError with op: 'resolve'.
Requirements
- Node.js >= 20
- TypeScript projects: types ship with the package (
dist/index.d.ts)
The contract
Every driver implements this interface. Keep call sites against MemoryDriver — never against vendor SDKs directly (unless you intentionally use the escape hatch).
interface MemoryDriver {
readonly name: string;
add(input: { userId: string; messages: { role: string; content: string }[] }): Promise<MemoryRecord[]>;
search(input: { userId: string; query: string; limit?: number }): Promise<MemoryRecord[]>;
get(input: { userId: string; limit?: number }): Promise<MemoryRecord[]>;
delete(input: { userId: string; id?: string }): Promise<void>;
raw(): unknown; // escape hatch — underlying vendor client
}Normalized MemoryRecord
All drivers return the same shape:
interface MemoryRecord {
id: string;
userId: string;
content: string;
metadata?: Record<string, unknown>;
createdAt: string; // ISO-8601
score?: number; // populated by search(); higher = more relevant
}Configuration
Programmatic config (supported today)
This is the only config path implemented in M1:
import { createMemory, defineConfig } from '@achref_hasni/membridge';
// Inline object — simplest path
const memory = await createMemory({
drivers: [{ name: 'mem0', options: { apiKey: process.env.MEM0_API_KEY } }],
});
// With defaultDriver when multiple backends are configured (M2+)
const cfg = defineConfig({
drivers: [
{ name: 'mem0', options: { apiKey: process.env.MEM0_API_KEY } },
// { name: 'zep', options: { apiKey: process.env.ZEP_API_KEY } }, // M2
],
defaultDriver: 'mem0',
});
const mem = await createMemory(cfg);defineConfig() is a typed identity helper — it gives editors full type-checking but does not load files.
Config file loading
MemBridge discovers and loads membridge.config.json, .js, or .mjs from the current directory:
membridge validate # auto-discover config in cwd
membridge validate -c ./my.json # explicit pathFrom code:
import { loadConfig, loadConfigFile } from '@achref_hasni/membridge';
const { config, configPath } = await loadConfig();
// or
const cfg = await loadConfigFile('./membridge.config.json');TypeScript config files (membridge.config.ts) are not loaded at runtime — import them from your app and pass the object to createMemory().
Config schema
Validated with Zod. Invalid config throws ZodError at parse time.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| drivers | { name, options }[] | yes (min 1) | Registered driver names + driver-specific options |
| defaultDriver | string | no | Which driver createMemory() picks when no override is passed. Must match a drivers[].name. |
| bench | object | no | Benchmark settings (schema only in M1; no runner yet) |
| bench.dataset | string | no | Path to benchmark dataset file |
| bench.costPer1kTokens | number | no | USD per 1k tokens for cost reporting |
| bench.replay | boolean | no | Default false. Run against cassettes instead of live backends (M3+) |
Mem0 driver options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| apiKey | string | process.env.MEM0_API_KEY | Mem0 platform API key |
| host | string | process.env.MEM0_HOST | Custom host (self-hosted / proxy) |
| organizationId | string | — | Mem0 organization ID |
| projectId | string | — | Mem0 project ID |
| client | unknown | — | Inject a fake/live client (tests & replay). Skips SDK construction. |
Swap the backend (when the driver exists)
const memory = await createMemory({
- drivers: [{ name: 'mem0', options: { apiKey: process.env.MEM0_API_KEY } }],
+ drivers: [{ name: 'zep', options: { apiKey: process.env.ZEP_API_KEY } }],
});
// Call sites using MemoryDriver stay identicalEnvironment variables
Copy .env.example to .env. Never commit real keys.
| Variable | Used by | Status |
|----------|---------|--------|
| MEM0_API_KEY | Mem0 driver | ✅ Read when options.apiKey is omitted |
| MEM0_HOST | Mem0 driver | ✅ Read when options.host is omitted |
| ZEP_API_KEY | Zep driver | ⏳ M2 |
| LETTA_API_KEY | Letta driver | ⏳ M2 |
| LETTA_BASE_URL | Letta driver | ⏳ M2 |
MemBridge does not load .env automatically. Use dotenv or your framework's env handling, or pass keys in config.
API reference
createMemory(config, driverName?)
Resolves a ready-to-use MemoryDriver from config.
import { createMemory } from '@achref_hasni/membridge';
const memory = await createMemory(config);
const memory2 = await createMemory(config, 'mem0'); // explicit overrideResolution order for driver name:
driverNameargument (if provided)config.defaultDriver- First entry in
config.drivers
Throws MemBridgeError (op: 'resolve') if the chosen name is not in config.drivers or not registered in the registry.
raw<T>(driver)
Escape hatch — returns the underlying vendor client with your type parameter:
import { raw } from '@achref_hasni/membridge';
import type MemoryClient from 'mem0ai';
const client = raw<MemoryClient>(memory);
await client.users(); // Mem0-specific call MemBridge does not wrapparseConfig(input) / defineConfig(config)
Validate or author config objects. parseConfig throws ZodError on invalid input.
registry / DriverRegistry
The default registry is pre-loaded with built-in drivers (currently mem0). Register custom drivers:
import { DriverRegistry, type DriverFactory } from '@achref_hasni/membridge';
const custom = new DriverRegistry();
custom.register('my-backend', myFactory as DriverFactory);
const driver = await custom.resolve('my-backend', { /* options */ });Importing @achref_hasni/membridge registers built-in drivers on the global registry as a side effect.
Errors — MemBridgeError
All driver operations wrap failures in a consistent shape:
class MemBridgeError extends Error {
readonly driver: string; // e.g. "mem0"
readonly op: 'add' | 'search' | 'get' | 'delete' | 'init' | 'resolve';
override readonly cause: unknown; // original backend error, preserved
}Config validation throws ZodError, not MemBridgeError — a known inconsistency at the config seam.
CLI reference
After pnpm build, the membridge binary is at dist/cli/index.js (also linked via package.json bin when installed globally).
membridge list
Lists registered driver names.
membridge list
# Registered drivers:
# - mem0membridge validate
Validates a config file and checks that every named driver is registered.
membridge validate
membridge validate -c ./membridge.config.jsonmembridge bench
Prints the benchmark comparison table preview (placeholder metrics until M3–M4). Exits with code 1 until the runner ships.
membridge bench┌──────────┬────────┬───────────┬───────────┬──────────────┬─────────────┐
│ driver │ recall │ p50 (ms) │ p95 (ms) │ $ / 1k convo │ footprint │
├──────────┼────────┼───────────┼───────────┼──────────────┼─────────────┤
│ mem0 │ — │ — │ — │ — │ — │
└──────────┴────────┴───────────┴───────────┴──────────────┴─────────────┘membridge parse
Validate an inline JSON config string (for scripting):
membridge parse '{"drivers":[{"name":"mem0"}]}'Architecture
src/
├── index.ts # Public entry: createMemory(), re-exports, driver registration
├── config.ts # Zod schemas + defineConfig / parseConfig
├── core/
│ ├── types.ts # MemoryDriver contract + normalized types
│ ├── registry.ts # DriverRegistry (name → factory)
│ ├── errors.ts # MemBridgeError + wrapOp()
│ └── escape-hatch.ts
├── drivers/
│ └── mem0/index.ts # Mem0 adapter (normalization, lazy SDK import)
└── cli/index.ts # Commander CLIRequest flow for memory.add():
createMemory() → parseConfig() → registry.resolve() → mem0Factory() → Mem0Driver.add() → wrapOp() → mem0ai clientSee CONTEXT.md for domain glossary and milestone definitions.
Writing a driver
Each new driver (M2+) must:
- Implement
MemoryDriver— all four CRUD methods plusraw() - Register a
DriverFactoryunder a stable name (e.g.'zep') - Validate driver-specific options with Zod
- Wrap backend errors with
wrapOp()→MemBridgeError - Normalize responses to
MemoryRecord - Lazy-import the vendor SDK so unused backends stay out of the install tree
- Support
options.clientinjection for recorded tests - Include a cassette-based integration test
Minimal skeleton:
import { wrapOp } from '@achref_hasni/membridge';
import type { DriverFactory, MemoryDriver } from '@achref_hasni/membridge';
export const myFactory: DriverFactory = async (options) => {
const client = options.client ?? await createLiveClient(options);
return {
name: 'my-backend',
add: (input) => wrapOp('my-backend', 'add', () => /* ... */),
search: (input) => wrapOp('my-backend', 'search', () => /* ... */),
get: (input) => wrapOp('my-backend', 'get', () => /* ... */),
delete: (input) => wrapOp('my-backend', 'delete', () => /* ... */),
raw: () => client,
};
};Testing & replay cassettes
Tests use an injectable client instead of live API calls:
const memory = await createMemory({
drivers: [{ name: 'mem0', options: { client: fakeMem0Client } }],
});Recorded fixtures live in test/fixtures/*.cassette.json. See test/mem0.test.ts for the full pattern.
Run tests:
pnpm test # single run
pnpm test:watch # watch modeCurrent coverage (14 tests):
- Config validation (
parseConfig) - Registry register / list / resolve / unknown driver
- Escape hatch (
raw()) - Mem0 CRUD normalization, request forwarding, error wrapping, missing API key
Not yet tested: CLI commands, createMemory edge cases (missing driver in config), live SDK import path, bench/replay engine.
Benchmark (roadmap)
The launch artifact — one command, one reproducible comparison table — lands in M3–M4:
membridge bench # not yet availablePlanned output:
┌──────────┬────────┬───────────┬───────────┬──────────────┬─────────────┐
│ driver │ recall │ p50 (ms) │ p95 (ms) │ $ / 1k convo │ footprint │
├──────────┼────────┼───────────┼───────────┼──────────────┼─────────────┤
│ mem0 │ … │ … │ … │ … │ … │
│ zep │ … │ … │ … │ … │ … │
│ letta │ … │ … │ … │ … │ … │
└──────────┴────────┴───────────┴───────────┴──────────────┴─────────────┘Every report will record hardware, driver versions, and the pricing assumption. Replay mode (bench.replay: true) will re-run against cassettes for CI without live keys.
Milestone status
| Milestone | Scope | State |
|-----------|-------|-------|
| M1 | Core contract, driver registry, zod config, Mem0 driver + recorded test | ✅ done |
| M2 | Zep and Letta drivers behind the same interface | ⏳ next |
| M3 | Benchmark runner + metrics (latency percentiles, $ cost, recall) | ◻️ planned |
| M4 | membridge bench → markdown + JSON comparison table | ◻️ planned |
| M5 | Docs site + comparison table as README hero | ◻️ planned |
Development
Setup
git clone https://github.com/AchrefHASNI/membridge.git
cd membridge
pnpm install
cp .env.example .env # optional — only needed for live Mem0 callsScripts
| Script | Command | Description |
|--------|---------|-------------|
| build | pnpm build | Bundle ESM + CJS + types via tsup |
| dev | pnpm dev | Watch mode rebuild |
| test | pnpm test | Vitest single run |
| typecheck | pnpm typecheck | tsc --noEmit |
| lint | pnpm lint | Biome check |
| lint:fix | pnpm lint:fix | Biome auto-fix |
| format | pnpm format | Biome format |
| audit | pnpm audit | Dependency audit |
Quality gate (CI-equivalent)
pnpm typecheck && pnpm test && pnpm lint && pnpm buildTech stack
TypeScript (strict) · Node ≥ 20 · pnpm · tsup · Vitest · Biome · Zod · Commander
Project layout
| Path | Purpose |
|------|---------|
| src/ | Library source |
| test/ | Vitest tests + cassettes |
| dist/ | Build output (published to npm) |
| website/ | Marketing/docs Vite app (not published in npm files) |
| CONTEXT.md | Domain glossary |
| .env.example | Environment variable template |
Troubleshooting
unknown driver "zep" (or letta)
Only mem0 is registered in M1. Zep and Letta drivers arrive in M2.
init failed: the "mem0ai" package is not installed
pnpm add mem0aimissing Mem0 API key
Set MEM0_API_KEY in your environment or pass options.apiKey in config. Empty string counts as missing.
Cannot find package 'commander' when running CLI locally
Run pnpm install first, then pnpm build. The CLI depends on runtime deps from node_modules.
Config throws ZodError
Check that drivers is a non-empty array and defaultDriver (if set) matches a driver name.
MEM0_HOST in .env has no effect
Ensure you load .env into process.env (MemBridge does not call dotenv). Then MEM0_HOST is used when options.host is omitted. Or pass host explicitly:
{ name: 'mem0', options: { apiKey: '...', host: 'https://your-mem0-host' } }Contributing
Driver requests and PRs welcome. Each new driver needs:
- The four contract methods +
.raw() - Normalized errors (
MemBridgeError) - A recorded integration test with cassette fixture
- Lazy optional SDK import
- Justification for any new dependency
Keep the dependency tree small. See Writing a driver.
Author
Built by Achref Hasni.
License
Apache-2.0 © 2026 Achref Hasni — chosen for the patent grant and maximum adoption.
