Skip to content

Architecture

memio is a unified memory gateway for AI agents. It lets you connect any combination of memory providers behind a single client, so your agent code never depends on a specific vendor SDK.

This page explains the design decisions that make that possible.

Protocol-based design

memio defines four store protocols using Python's @runtime_checkable Protocol from the typing module:

Protocol Purpose
FactStore Structured facts scoped to a user or agent
HistoryStore Conversation message history grouped by session
DocumentStore Documents with semantic search and metadata filtering
GraphStore Knowledge graph triples (subject-predicate-object)

Because these are protocols -- not base classes -- any class that provides the right async methods satisfies the contract. No inheritance is required. The Memio client validates each store with isinstance() at init time and raises TypeError if the check fails.

from memio import Memio
from memio.providers.mem0 import Mem0FactAdapter

# Mem0FactAdapter has no base class -- it just implements
# the same async methods that FactStore declares.
client = Memio(facts=Mem0FactAdapter(api_key="..."))

The adapter pattern

Each provider ships one or more adapter classes. An adapter translates memio's protocol methods into the provider's own SDK calls and normalizes responses into memio's data models:

Model Fields
Fact id, content, user_id, agent_id, metadata, score, created_at, updated_at
Message role, content, metadata, timestamp, name
Document id, content, metadata, score, created_at, updated_at
Triple subject, predicate, object, metadata
GraphResult triples, nodes, scores

Your agent code works exclusively with these models. If you swap providers, you change one import and one constructor call -- the rest of your code stays the same.

Architecture diagram

Your AI Agent
     |
     v
   Memio Client
     |
     |-- facts     -> Mem0FactAdapter        -> Mem0 Cloud API
     |-- history   -> ZepHistoryAdapter      -> Zep Cloud API
     |-- documents -> DocumentStore adapters -> Vector DBs (Qdrant, Chroma, ...)
     |-- graph     -> ZepGraphAdapter        -> Zep Cloud API

Composability

You can mix providers freely. The Memio client accepts any combination of stores -- use whichever provider is strongest for each memory type:

from memio import Memio
from memio.providers.mem0 import Mem0FactAdapter
from memio.providers.zep import ZepHistoryAdapter
from memio.providers.chroma import ChromaDocumentAdapter

import chromadb

client = Memio(
    facts=Mem0FactAdapter(api_key="mem0-key"),
    history=ZepHistoryAdapter(api_key="zep-key"),
    documents=ChromaDocumentAdapter(
        client=chromadb.PersistentClient(path="./chroma_data"),
        collection_name="docs",
    ),
)

At least one store must be provided. Stores you do not need can be omitted -- accessing an omitted store attribute returns None.

Provider support matrix

Store Mem0 Zep Chroma Supermemory Letta Qdrant
FactStore Mem0FactAdapter ZepFactAdapter -- SupermemoryFactAdapter LettaFactAdapter --
HistoryStore -- ZepHistoryAdapter -- -- -- --
DocumentStore -- -- ChromaDocumentAdapter SupermemoryDocumentAdapter LettaDocumentAdapter QdrantDocumentAdapter
GraphStore Mem0GraphAdapter ZepGraphAdapter -- -- -- --

Error handling

Every adapter wraps provider SDK exceptions in a single error type:

from memio.exceptions import ProviderError

try:
    fact = await client.facts.add(content="likes coffee", user_id="alice")
except ProviderError as e:
    print(e.provider)   # "mem0"
    print(e.operation)  # "add"
    print(e.cause)      # the original SDK exception

ProviderError always carries three attributes:

  • provider -- the name of the provider ("mem0", "zep", "chroma", "supermemory", "letta", "qdrant").
  • operation -- the protocol method that failed ("add", "search", etc.).
  • cause -- the original exception from the provider SDK.

This means your error-handling code never needs to import provider-specific exception types.

Async-first

All store operations are async/await. Every method on every protocol is declared async def, and every adapter implements them as coroutines:

# Every call goes through await
fact = await client.facts.add(content="likes coffee", user_id="alice")
results = await client.facts.search(query="coffee", user_id="alice")
await client.facts.delete(fact_id=fact.id)

This makes memio a natural fit for async frameworks, LLM tool-calling loops, and any agent runtime that uses asyncio.

Multi-tenancy

FactStore and GraphStore support user_id and agent_id parameters for scoped data access. This lets a single memio deployment serve multiple users or agents without data leaking across boundaries:

# Facts scoped to a specific user
await client.facts.add(
    content="prefers dark mode",
    user_id="alice",
)

# Search only returns facts for this user
results = await client.facts.search(
    query="UI preferences",
    user_id="alice",
)

# Graph triples scoped to a user
await client.graph.add(
    triples=[Triple(subject="Alice", predicate="works_at", object="Acme")],
    user_id="alice",
)

HistoryStore scopes data by session_id, with get_all accepting a user_id to list all sessions for a given user.