Python SDK for agent memory, compared: eleven packages, twelve rows
This is the written half of a video, Python SDK for agent memory, compared, and the sections below follow its eight chapters in order, so anyone arriving from the end card can pick up where they stopped.
Updated 13 September 2026. A second check against each vendor's own pages found claims here that were too broad or too narrow, and each is corrected where it stands. The dependency list shows whether the memory engine ships in the package, which is not the same as where the memory runs, and that holds for our own row too. Letta's client constructs without a key, and Supermemory's hosted MCP server signs you in with OAuth. Competitors now get what their own pages say: Mem0's free tier and verbatim writes, Zep's single line to add and to retrieve memory and its search that returns edges by default, Graphiti's embedded stores and its nodes and communities, the servers Supermemory and Letta let you run yourself, Supermemory's file uploads, Cognee's remote connect call and its image and audio ingestion, and the local path Mem0 and Cognee both publish openly. The memory functions of the cognee package are async, as Graphiti's class is, and a read returns ten different answers here, not six. Two quotations, from Letta's README and from Mem0's plugin source, now match what those files say today. About us: the description of our memory server's repository was rewritten on 12 September 2026, and this page no longer describes it in the present tense.
Updated 20 September 2026. Our own row now reads release 0.3.0 as well: its wheel size, and the line in its sync client that says what that client wraps. The 8 September reading of 0.2.0 stays beside them, dated. Three em dashes are gone, two of them in our own prose, and so is the footer clause that counted them as one.
Eleven packages, twelve rows. The row count is higher than the package count because pip install mem0ai is two rows: the class Memory and the class MemoryClient come out of the same install and are two different products.
Every vendor here leads with a one line install command, and not one of those commands gives you working memory. That is a claim about the install command, not about lines of code: Zep's memory page says "you add memory with a single line, retrieve memory with a single line", and Supermemory's hosted MCP server needs one server address and a browser sign-in, with no API key. The install command is the fact every front page puts first and the least informative fact about any of these packages. Three other things, all published, none advertised, tell you what you actually bought, and all three are faster than reading a landing page.
TL;DR
- Check one, the runtime dependency list on PyPI. It answers whether the package only builds requests or ships the memory engine, and it is readable before you install anything, in the JSON metadata PyPI serves rather than on the project page with the install command. Read on 8 September 2026, with extras excluded:
mnemoversedeclares two,zep-cloudfive,letta-clientsix,supermemorysix, and not one of those four lists contains a store, a database driver or a model client.graphiti-coredeclares seven including a graph database driver and a model client,mem0aieight including a vector store client and a database toolkit,cogneeforty five lines, of which an install takes forty three or forty four depending on the platform, including an embedded vector store, an embedded graph database and a web server.- Check two, the class names the package exports. One package here ships two products and the import line decides which one you get, with the vendor stating the routing in its own machine readable index.
- Check three, construct the client with nothing and read what comes back. Most of the clients raise, and what each asks for first is not the same thing, nor always everything it needs: Graphiti's first refusal asks for a database address or a graph driver, and its default path wants an OpenAI key after that. Two construct silently, and one of those two is already pointed at Letta's cloud.
- The trade is not a key against no key, and it is not one decision either. The model and the index are placed separately: Graphiti's default runs the graph on your machine and calls a hosted model, while Mem0's documented local recipe runs both on yours. Persisting with no vendor credential is documented by Mem0, Cognee, Graphiti and Letta, and Supermemory's self-hosted binary runs offline with a key it generates for you; none of it is hidden, since Mem0's comparison page lists the local path and Cognee has a guide titled Local Setup (No API Key). What it costs differs more than the choice does.
- What no install command shows is what a read gives back, and that is where these stop being the same category. Ten different answers behind one word, across eleven rows.
- Disclosure is in the next section rather than the footer: we publish one of these packages, its row is read here more strictly than anyone else's, and two things we published did not survive that check: our first release's default address, corrected in the next release, and the one line description of our memory server's repository, rewritten on 12 September 2026.
- Version numbers date faster than anything else here. Every version below was re-read from PyPI on 8 September 2026, except our own 0.3.0, uploaded on 16 September and read after that, and the date sits next to each one. The three checks are the part that survives the next release.
Disclosure, and it goes first
We build one of the packages in this table. It is mnemoverse on PyPI, it is a hosted client, and its row is read here more strictly than anyone else's, because the argument of this piece is that what a vendor publishes is checkable, and two things we published did not survive that check: one in the package, one about the memory server we also publish.
We found the first one ourselves and wrote it down. Our changelog for 0.2.0 records that the previous release shipped a default address pointing at "a different host that answers 401", and states the consequence on the next line: "so a default-configured 0.1.0 client could not reach the service at all." The default was corrected in 0.2.0, and 0.1.0 had been the only published release for four months.
The second was not on this package's repository. It was on the memory server we also publish: when this article went out on 8 September 2026, the one line GitHub prints for github.com/mnemoverse/mcp-memory-server, in the page title and in the meta description, carried two claims our own documentation had withdrawn. The README and the description of the latest npm release no longer carried them; the repository description did, and it is the first line a stranger reads. It was rewritten on 12 September 2026, and what it says now is in the section on our own row below. The withdrawn sentence is neither reprinted nor paraphrased here, because repeating a withdrawn claim inside a new article is how a withdrawn claim gets back into circulation.
That is the disclosure. It is also the method: everything below was read the same way, from what the vendor publishes, on a named date, with the counting rule stated.
Corrections to the first cut of the video, ours first
The first cut of the video says our client raises on a precondition rather than on a key. It does not, in either release, and in the 0.2.0 wheel the video was made from, the true version was stricter than the one we said.
Constructed with nothing, from the 0.2.0 wheel, MnemoClient() raised a plain TypeError: MnemoClient.__init__() missing 1 required positional argument: 'api_key', and AsyncMnemoClient() raised the same with its own name. The string precondition does not occur in any of the six modules of the package, in 0.2.0 or in 0.3.0; the four error classes it defines are MnemoError, MnemoAuthError, MnemoRateLimitError and MnemoUnavailableError. So the refusal was not a message we wrote at all. It was the language refusing, because the requirement was expressed in the signature, and 0.2.0 had no environment fallback anywhere in the package to soften it. In 0.3.0 that fallback exists, and the refusal, when it comes, is our own sentence naming both the argument and the variable.
One sentence in that chapter of the first cut also needs narrowing, and this article uses the narrow form throughout: it is not true that every vendor here states its terms in the first exception, because two of the twelve rows construct with no exception at all. What each of those two silences means is in the constructor section below. The first cut also carried claims about other vendors that were too broad or too narrow; this page states each of them the way the vendor's own pages do, and the note at the top lists what changed.
The install line is the least informative thing here
Twelve rows, eleven packages, and eleven install commands that all look alike. Here is the whole field, with every version and every dependency count re-read from pypi.org/pypi/<name>/json on 8 September 2026.
| package | version | uploaded | runtime dependencies, extras excluded | where the memory runs by default |
|---|---|---|---|---|
mnemoverse (ours) | 0.2.0 (0.3.0 since 2026-09-16) | 2026-08-14 | 2 | ours, hosted only |
mem0ai, the Memory class | 2.0.20 | 2026-09-02 | 8 | yours, in your process or as a self-hosted server, with OpenAI as the default model |
mem0ai, the MemoryClient class | 2.0.20, same package | 2026-09-02 | 8 | Mem0's hosted platform |
letta-client | 1.12.1 | 2026-06-02 | 6 | Letta's by default, yours if you set base_url |
zep-cloud | 3.28.0 | 2026-08-11 | 5 | Zep's cloud, or your own cloud through Zep's BYOC offer for enterprise customers |
graphiti-core | 0.30.1 | 2026-09-01 | 7 | yours, plus a graph database you run or an embedded one behind a pip extra, with OpenAI as the default model |
cognee | 1.5.4 | 2026-09-04 | 45 declared lines, 44 distinct names, 42 with no platform marker | yours, with the stores embedded and OpenAI as the default model, or Cognee Cloud once you call serve() |
supermemory | 3.61.0 | 2026-09-05 | 6 | their server, or a binary you run |
langgraph | 1.2.11 | 2026-08-11 | 6 | yours |
langmem | 0.0.30 | 2025-10-27 | 8 | yours, with a model called remotely |
llama-index-core | 0.14.24 | 2026-08-19 | 29 | yours |
deepagents | 0.7.13 | 2026-09-02 | 7 | yours, as files |
Two notes on that table before anything is read into it.
The counting rule, because the same field gives two very different answers. Every requirement line carrying an extra == marker was dropped; platform markers such as sys_platform and platform_version were kept, because they are not extras. Counted with the extras left in, the same packages read mnemoverse seven, letta-client eight, supermemory eight, graphiti-core forty seven, mem0ai fifty six, cognee one hundred and twenty nine. Only zep-cloud gives the same number both ways. The number without extras is the one closest to what a bare pip install puts on your disk, and it is the number in the table. Where a line carries a platform marker, an install takes it only on that platform: of Cognee's forty five lines, forty two carry no marker, and an install resolves forty four on Windows and forty three on Linux and macOS.
A dependency count is a structural fact about what gets installed, not a quality score. A package with forty five requirement lines is not worse than a package with two. It is doing more work on your machine.
What the dependency list tells you first
requires_dist is published by every project on PyPI, in the JSON the index serves at pypi.org/pypi/<name>/json rather than on the project page that shows the install command, and it settles the largest question in this comparison before you type anything.
Four of these declare a handful of dependencies, and not one of the four lists contains a store, an index or a model. Ours declares two, httpx and pydantic. Zep's cloud package declares five: httpx, pydantic, pydantic-core, python-dateutil, typing_extensions. Letta's client declares six and Supermemory's declares six, and here is a detail worth its own sentence, because it explains the shape of both: those two lists are identical, name for name, anyio, distro, httpx, pydantic, sniffio, typing-extensions. Both clients come out of the same code generator. A package with no store, no database driver and no model client in its dependency list is a request builder. It assembles HTTP requests, and the memory runs in a server outside the package. Whose server varies: Supermemory documents a self-hosted server that "speaks the same API as the hosted platform", Letta's client names a local address, http://localhost:8283, beside its cloud one, Zep offers "BYOC deployments where Zep runs in your own cloud infrastructure" to enterprise customers, and ours is hosted only.
Three of them declare something quite different. graphiti-core declares seven, and two of the seven are neo4j>=5.26.0 and openai>=1.91.0: a graph database driver and a model client. mem0ai declares eight, including qdrant-client>=1.12.0 and sqlalchemy>=2.0.31: a vector store client and a database toolkit. cognee declares forty five lines, and the shape of that list is the argument. lancedb is an embedded vector store. ladybug is the graph database, and it is declared twice under mutually exclusive platform markers, which is why the forty five lines resolve to forty four distinct names. fastapi, starlette, uvicorn and gunicorn are a web server. Those three ship the memory engine in the package, which is not the same as running entirely in one process on your machine: each calls a hosted model by default, Graphiti's documented prerequisite is a Neo4j or FalkorDB server you run unless you install an embedded one behind a pip extra, such as Kuzu, and Mem0 documents a self-hosted server mode beside the library, while its default stores and Cognee's sit on local disk. That is a different failure mode, a different bill and a different thing to operate, and the dependency list tells you before you install anything.
One naming detail that will trip anyone re-checking Cognee, and both names are the vendor's own. The installation page says you get "Cognee installed with default local databases (SQLite, LanceDB, Kuzu)", while the requirement list and the pip output say ladybug, a name that also appears on the vendor's pages. The string kuzu does not occur in any requirement line of the shipped 1.5.4 package, and neo4j occurs there only behind an extra.
And the caution that keeps the rule honest. The rule is about the contents of the list, not its length. llama-index-core declares twenty nine runtime dependencies, more than graphiti-core and mem0ai combined, and it is a framework rather than a memory engine. Read for a store, an index, a database driver or a model client. Do not read for a big number.
One package, two products, and the import line decides
There is a case in this table where the install command genuinely cannot tell you what you bought, because one package ships two products.
mem0/__init__.py in the shipped 2.0.20 wheel sets a version string and then does all of its public routing in two import lines:
from mem0.client.main import AsyncMemoryClient, MemoryClient # noqa
from mem0.memory.main import AsyncMemory, Memory # noqaFour class names, two products. The vendor states the routing itself, in the machine readable index it publishes for answer engines at docs.mem0.ai/llms.txt, on two adjacent lines: "Use MemoryClient (Python) / mem0ai (npm) when the user has a Mem0 Platform API key." and "Use Memory (Python) / mem0ai/oss (npm) when the user self-hosts." Same package, same install line, and the import decides whether your memory goes to Mem0's managed Platform, which has a free tier, or runs the same engine on your own infrastructure. One boundary on every Mem0 claim on this page, stated here rather than in a footnote: our six surface sweep of 7 September 2026 did not capture a changelog for Mem0, so nothing here rests on one, and no absence is claimed about that surface.
The reverse case is the pair that gets quoted as though it were one product. Zep's cloud package is the hosted client: five dependencies, no store, no model, and its entire environment enum is a single hosted address, DEFAULT = "https://api.getzep.com/api/v2". Graphiti is the engine, which Zep's own FAQ calls "The open-source Context Graph framework that powers Zep Cloud"; it is a separate package, and it needs a graph database, either one you run or an embedded one behind a pip extra, such as Kuzu, which Graphiti's docs call "an embedded graph engine that does not require any additional setup". The same sweep did not capture a changelog for Zep either, so the same boundary applies to every Zep and Graphiti claim here.
Graphiti gives you no choice between sync and async. Imported and inspected on 8 September 2026, the top level of graphiti_core exposes exactly one class name, Graphiti; there is no AsyncGraphiti and no synchronous twin. It is not alone: the memory functions of the cognee package are async as well, and Cognee says so itself: "Cognee uses asynchronous code extensively." The Mem0, Letta, Zep, Supermemory and Mnemoverse clients each ship both, as a pair of names differing by the word async: MnemoClient and AsyncMnemoClient, Memory and AsyncMemory, MemoryClient and AsyncMemoryClient, Letta and AsyncLetta, Zep and AsyncZep, Supermemory and AsyncSupermemory. If your agent loop is synchronous, Graphiti asks you for an event loop before you store anything.
And one more thing that rides inside the same wheel, because it shows why the artifact beats the prose about the artifact. In graphiti_core 0.30.1 the shipped code sets its concurrency ceiling as SEMAPHORE_LIMIT = int(os.getenv('SEMAPHORE_LIMIT', 20)), and the long description packed into the very same archive, the one PyPI renders on the project page, says "By default, SEMAPHORE_LIMIT is set to 10". Both were read out of one downloaded file on 8 September 2026. Which one is intended is the vendor's to say and this article does not guess; what it shows is that a page and a package can disagree without either being hidden, and that the package is the one your process will obey.
So before installing, two lines of somebody else's published material have already told you whether the engine ships in the package and which of a vendor's products you are about to get.
What an empty constructor asks for first
The third check needs a terminal rather than a web page. Construct the client with no arguments and read what comes back. Every one of these was run on 8 September 2026, in a clean virtual environment on Python 3.11.9, from the wheel downloaded from PyPI, with every relevant environment variable cleared first.
| construct with nothing | what comes back |
|---|---|
MnemoClient(), ours | TypeError: MnemoClient.__init__() missing 1 required positional argument: 'api_key' |
MemoryClient(), Mem0 hosted | ValueError: "Mem0 API Key not provided. Please provide an API Key." |
Memory(), Mem0 open source | openai.OpenAIError: "Missing credentials. Please pass an api_key, workload_identity, admin_api_key, or set the OPENAI_API_KEY or OPENAI_ADMIN_KEY environment variable." |
Zep() | ApiError wrapping "The client must be instantiated be either passing in api_key or setting ZEP_API_KEY" |
Supermemory() | SupermemoryError: "The api_key client option must be set either by passing api_key to the client or by setting the SUPERMEMORY_API_KEY environment variable" |
Graphiti() | ValueError: "uri must be provided when graph_driver is None" |
Letta() and AsyncLetta() | constructs, no exception |
InMemoryStore(), LangGraph | constructs, no exception |
Memory(), LlamaIndex | ValidationError: "Value error, Token limit must be set and greater than 0." The documented Memory.from_defaults() constructs with no credential at all |
cognee | no exported client class on its local path; the key requirement appears when an operation calls the model. Its remote connect call, cognee.serve(), was not run here: given nothing in a clean environment, its 1.5.4 source raises an error asking for the instance URL and API key, and its docs add a Cloud login flow only where a device client ID is configured |
langmem | no client class; ten exported names, of which eight are factories |
deepagents | no client class; memory arrives as middleware |
Read down that column and several things fall out that no feature list carries.
The strongest of them belongs to Mem0, and it is the same install twice. pip install mem0ai produces two first exceptions written by two different companies. The hosted class refuses in Mem0's own words. The open source class gets as far as wiring up its default model provider and then fails with a message from the openai library, because on its default path Mem0 calls a model during ingestion. Nothing states "one package, two products" more plainly than two vendors answering the same install command. The exact wording of that second message belongs to the openai library and can change with its version, which is why it carries the date it was read.
Graphiti's first refusal is not about an account. It asks for a database address or a graph driver, and its default path then wants an OpenAI key, which Graphiti's quick-start lists among its requirements. The address is not needed once a driver is given: the embedded Kuzu driver's database path defaults to an in-memory one. That first refusal still answers the question this article says to ask first: the engine is in the package, and the store is something you point it at.
Cognee's version of the same moment is documented rather than raised in a constructor, and the vendor names its own exception on a page headed "Local Setup (No API Key)": "LLMAPIKeyNotSetError: LLM API key is not set on a fully local setup".
Ours was the strictest of the eleven in the release this table read, and not because we wrote a better message. In 0.2.0, read on 8 September, api_key was a positional parameter with no default in both clients, a full grep of the shipped wheel found no getenv, no environ and no dotenv anywhere in it, and running the constructor with MNEMOVERSE_API_KEY and MNEMO_API_KEY both set still failed. What you got was the language's own error rather than a sentence explaining anything.
That changed in 0.3.0, uploaded to PyPI on 16 September 2026, after this comparison was written. The key is now optional, the client reads MNEMOVERSE_API_KEY when no argument is passed, and a constructor given neither raises ValueError: No API key: pass api_key= or set the MNEMOVERSE_API_KEY environment variable (checked in a clean virtual environment on 17 September 2026; os.environ appears in _async_client.py, dotenv still nowhere). On the current release we behave the way Mem0, Zep and Supermemory do in this table: read an environment variable before complaining.
And the two silences, which are terms as well. Letta() constructs with nothing, and the reason it can is in the same file as the class: the client's environment table names two addresses on consecutive lines, "cloud" at https://api.letta.com and "local" at http://localhost:8283, cloud first. A silent constructor here means an object already pointed at a hosted service; the local address is one keyword argument away, and choosing it is your move to make rather than the default. Its terms arrive with the first request instead: sent with no key, a request to the cloud comes back 401, and the refusal names the self-hosting address as well. InMemoryStore() constructs with nothing because there is nothing to authenticate against: it is a dictionary. LlamaIndex is a third shape again, where the bare Memory() fails on an unset token limit rather than on any credential.
Four minutes of that in a terminal tells you more than four front pages. Not because a first error names everything, since Graphiti's names the driver and not the model key that comes after it, but because what it does name is a requirement no front page carried.
What it costs to persist, and who runs it
Here is the part this comparison nearly got wrong, and the wrong version is worth stating because it is the tidy one people repeat: that everything which survives a restart needs a key, and everything which needs no key does not survive a restart. That is false, and the vendors' own pages are what refute it.
The trade is not a key against no key, and it is not one decision either: the model and the index are placed separately. Graphiti's default path runs the graph on your machine and still wants an OpenAI key; Cognee ships an embedded vector store and graph database and documents a setup with no key at all; Mem0's local recipe puts both the model and the index on your machine. Every hosted client class in this table needs a key, and all but Letta's say so in the constructor; Letta's builds without one and points at its cloud. Some hosted MCP servers sign you in instead: Supermemory's says it "uses OAuth, so no API key is required." Persisting with no vendor credential is documented by Mem0, Cognee, Graphiti and Letta, Letta through an npm install rather than pip, and Supermemory's self-hosted binary runs offline with an API key it generates for you. Ours is not among them: it is hosted only, and its Enterprise plan runs on dedicated instances we provide and operate, with custom data residency. None of this is hidden. Mem0's own comparison page lists "local LLMs (Ollama), self-hosted vector DBs, no usage-based billing" among the reasons to choose open source, and Cognee has a guide titled "Local Setup (No API Key)". What it costs varies far more than the choice does.
Mem0 publishes a recipe for exactly this, and it is not buried on an editor integration page. It is indexed in the vendor's own llms.txt as "Local Companion (Ollama)", tagged [OSS], the only [OSS] entry among the seven recipes in the AI Companions section of that index. The page itself is headed "Self-Hosted AI Companion", so the two names differ and both are the vendor's, and its opening line is unambiguous: "Mem0 can be utilized entirely locally by leveraging Ollama for both the embedding model and the language model (LLM)." There is no key anywhere in its configuration. What it costs is two servers on your machine, a Qdrant on localhost:6333 and an Ollama on localhost:11434.
Cognee's floor is lower, because its stores are already inside the package. With lancedb and ladybug in the base requirements, what you supply is a model and an embedder rather than a store and a model. Its troubleshooting page, headed "Local Setup (No API Key)", is the sharpest statement of the trade anywhere in this field, and it cuts both ways in the same paragraph. First: "running it locally with Ollama needs no account, subscription, or paid API key", followed by "You are not being asked to pay for anything." Then, a few lines later: "The error appears because Ollama is one of the providers Cognee requires a non-empty LLM_API_KEY for, even though Ollama itself ignores the value." The remedy the vendor gives is "The fix is to set any placeholder string". A placeholder is not a purchase. It is not nothing either, and anyone who reads only the first of those two sentences will file a bug.
And the provider count that goes with it, in the only form that survives checking. Cognee's _API_KEY_REQUIRED_PROVIDERS set names six of the ten values in its provider enum: OPENAI, OLLAMA, CUSTOM, GEMINI, MISTRAL, ANTHROPIC. The documentation lists twelve providers and adds "These names are not an allowlist.", because anything behind an OpenAI-compatible endpoint goes through the custom value. It does not follow that the other four enum values are key free. AZURE is outside the set and still raises the same error unless managed identity is switched on. Exactly three never ask for a key string in that code: BEDROCK, LLAMA_CPP and MCP_SAMPLING. Those three are also not interchangeable. Bedrock is a hosted Amazon service that wants AWS credentials instead. MCP sampling borrows the host's model and only works where the host grants it. The one that is genuinely local and genuinely key free is llama.cpp loaded in your own process.
One more inversion, because it decides where people actually land. It is easy to conclude that a protocol server means hosted memory. It does not. Mem0's hosted MCP server says so in the vendor's own words: "Nothing runs on your machine: the server is hosted by Mem0, and your client connects to it over HTTPS." Cognee's, in standalone mode, says the opposite about itself: "the server runs the entire Cognee pipeline (ingestion, graph building, and search) locally with its own database; no extra Cognee install or running backend is required". Same protocol, opposite answers, and what separates them is the same dependency list from check one.
The neatest illustration of that is a server that is local and a memory that is not. Mem0's Claude Code plugin ships a genuine stdio protocol server; its own file says "Expose Mem0's memory search as one local coding-agent tool.", and it reads JSON-RPC from standard input. The memory it searches is not local: the plugin's core module sets DEFAULT_API_URL = "https://api.mem0.ai" and sends an authorization token with every call. The command runs on your machine, and check one applied to the plugin's own source tells you the memory does not.
Said carefully, because the obvious sentence about that is not true. Among the MCP servers indexed in Mem0's llms.txt and shipped in its mem0ai/mem0 repository on 13 September 2026, none runs against the open source Memory class, and that is the whole of the safe sentence. The flat version of it fails a two minute check by a reader, because the server that did run against that class, OpenMemory, is still readable in the vendor's own mem0ai/openmemory repository under openmemory-archive/, where the note reads "is no longer actively maintained. This directory is a read-only snapshot kept for reference." Our sweep of 7 September 2026 did not capture a changelog for Mem0, so nothing in this paragraph rests on one. Nor is it true that every path through an editor ends at the hosted Platform: that holds for the MCP paths, where the Claude Code page's first prerequisite is "A Mem0 Platform account and API key", but the plugin path documented for OpenClaw carries a section headed "Open-Source Mode (Self-hosted)" and states plainly "No Mem0 key is needed." Its own note on going further is "For a fully local setup, use Ollama for both."
The two rows that need no credential out of the box are the two that do not persist, and both vendors say so before anyone else can. LangGraph's InMemoryStore constructs instantly and is a Python dictionary, and the comment in the vendor's own code sample on the long term memory page says what to do about it: "InMemoryStore saves data to an in-memory dictionary. Use a DB-backed store in production use." LlamaIndex's default memory is an in-memory SQLite database, which is a file path that is not a file: "By default, the Memory class is using an in-memory SQLite database. You can plug in any remote database by changing the database URI." Working instantly and forgetting are the same fact about the same default.
Our own row, checked hardest
mnemoverse 0.2.0, uploaded to PyPI on 14 August 2026, re-read there on 8 September 2026. Two runtime dependencies, httpx>=0.25.0 and pydantic>=2.0.0, and nothing else; the single extra it declares is a development one, five test and lint tools, so even counted with extras there is no store, no index and no model in it. The 0.2.0 wheel is 13,468 bytes and pure Python. It ships a typed marker. The Python floor is 3.10. The classifier the package sets for itself is Development Status :: 4 - Beta. Two releases existed at that reading: 0.1.0 on 9 April 2026 and 0.2.0. A third, 0.3.0, followed on 16 September 2026, with the same two runtime dependencies and a wheel of 18,427 bytes, re-read on PyPI on 20 September 2026.
A small thing that belongs in an article about checking published facts. That release carries two dates, and both are ours. PyPI records 0.2.0 as uploaded on 14 August 2026. Our own changelog heads the same release 2026-08-13. Neither is wrong, they are the tag and the upload, and a piece that tells you to read what vendors publish should not quietly pick one.
The dependency list shows that no memory engine ships in this package; that the service behind it is hosted only is our own statement, which no dependency list can show. Both clients issue HTTP requests to https://core.mnemoverse.com, and one call does not need the key, the health call, which our own reference says skips the database round trip deliberately.
It ships both a synchronous and an asynchronous client, and the relationship between them is stated in the sync client's own class docstring instead of being inferred. The opening of it, read from mnemoverse/client.py at 0.3.0 on 20 September 2026:
class MnemoClient:
"""Synchronous client for the Mnemoverse Memory API.
Wraps AsyncMnemoClient for use in scripts, notebooks, and sync applications.In the release this table read, the key was required more strictly than anywhere else in it. In 0.2.0: positional, no default, no environment fallback, in both clients, with every request carrying it as a header. Since 0.3.0 the argument is optional and falls back to MNEMOVERSE_API_KEY, and a constructor given neither raises our own ValueError. In both releases the client checks only that a key is present, never that it is valid: the server does that, and refuses a bad one with a 401, which the client then raises as MnemoAuthError (re-run on 0.3.0 on 19 September 2026 with a deliberately invalid key: MnemoAuthError: Invalid or revoked API key.; the text after the colon is the server's, and the engine changed it the next day: the same run on 20 September 2026 gives MnemoAuthError: This API key is not recognized. Check that the whole key was copied, or create a new key at https://console.mnemoverse.com/dashboard/keys., and the 401 body names the cause in details.reason, see Reasons for a 401). As the correction above records, the exception an empty 0.2.0 constructor gave was the language's, not ours.
Now the two things we published that did not survive the check.
The first we found and wrote down, and the next release corrected it. It is the default address in 0.1.0, quoted from our changelog in the disclosure above, and it is a historical statement rather than a probe you can re-run: on the paths the 0.1.0 client calls, that host answers 404 today, from a different service, so anyone re-checking will see a different code than the changelog names. 0.1.0 itself is still on PyPI and has not been yanked, so a project that pins it still gets that default.
The second was live when this was published, and the line was rewritten on 12 September 2026. It was worse than the first for exactly the reason this comparison exists: it was not a defect in the package, it was a defect in what we publish. What it said, and where, is in the disclosure above.
Checked again on 2026-09-13: the repository's description, and the page title after the repository name, now begin "Hosted persistent memory for AI agents over MCP. Tell it a recalled memory helped or misled, and it re-ranks what comes back next.", and the retracted wording is absent from both.
This piece argues that what a vendor publishes is checkable, and that line was rewritten after this article named it.
What a read gives back, which is where they actually differ
Every product here takes content in, though not always the same content or through the same call: Supermemory names "two entry points: client.add() for text and URLs, client.documents.uploadFile() for actual files", and Cognee's cloud ingestion takes images and audio as well as documents. Reading is where they stop being the same category, and no install command and no feature list shows it. This is the column to compare.
| package | what a read hands back | where that is stated |
|---|---|---|
mnemoverse (ours) | ranked stored text | our reference documents content as "Stored text content" and relevance as "Final score (similarity * valence modulation)" |
mem0ai | extracted fact strings with a score by default; a documented flag stores content exactly as provided | the vendor: "By default, Mem0 stores extracted memories, not a verbatim transcript" and "Use infer=False when you need to store raw content exactly as provided." |
letta-client | passages carrying an identifier, content, a timestamp and tags | letta_client/types/passage_search_response.py declares score: float with the docstring "Relevance score" |
zep-cloud | a prompt-ready context block from the high level read; the low level graph.search returns ranked edges by default | the shipped ThreadContextResponse docstring says "Meant to be replaced in the system prompt on every chat turn"; the current docs say "Do not insert the context block into a system or developer message." |
graphiti-core | ranked graph edges from its basic search, each a fact, some with a validity window; its fuller search also returns nodes and communities | EntityEdge declares fact and episodes, and valid_at, invalid_at and expired_at as optional |
cognee | typed objects whose text is usually a generated answer | search() defaults to SearchType.HYBRID_COMPLETION |
supermemory | ranked memories or document chunks, each with a similarity score and a version number, and related memories on request | similarity: float, which the 3.61.0 SDK's search_memories_response.py documents as "Similarity score between the query and memory entry", plus a context object; the vendor: "In hybrid mode, results contain either a memory field (extracted facts) or a chunk field (document content), depending on the source." |
langgraph store | your own JSON back, by namespace and key | the vendor: "save data as JSON documents organized by namespace and key" |
langmem | nothing directly; it writes extracted items into a LangGraph store | ten exported names, eight of them factories, and no client class |
llama-index-core | a list of chat messages, with the long term part injected into them | the Memory object binds a read to one session |
deepagents | files | the vendor: "the agent reads and writes memory as files" |
Four of those deserve a sentence each, because the difference is bigger than a table row can carry.
Zep's high level read hands you a block Zep has assembled, and that is a product decision. The response type of thread.get_user_context has exactly one declared field, and the docstring in the shipped 3.28.0 SDK says what it is for: "Context block containing relevant facts, entities, and messages/episodes from the user graph. Meant to be replaced in the system prompt on every chat turn." Zep's current documentation places it differently: "Do not insert the context block into a system or developer message." The docstring and the docs disagree on that point. The block is also not the only read and not a fixed format: a context template passed as template_id sets what goes into it, and the low level graph.search returns ranked edges, its scope documented as "Defaults to Edges."
Graphiti's is the opposite decision. Its basic search gives you ranked edges, its fuller _search() returns "a list of nodes, edges, and communities", and you assemble the prompt yourself. The shipped 0.30.1 wheel declares fact as "fact representing the edge and nodes that it connects", valid_at as "datetime of when the fact became true", invalid_at as "datetime of when the fact stopped being true", and episodes as "list of episode ids that reference these entity edges", with the two datetimes optional, and Graphiti's own quick-start checks for valid_at before printing it. A provenance field on every edge, a validity window where one was extracted, and the assembly is your job.
Cognee's answer is a parameter rather than a property. Its SearchType enum in the shipped 1.5.4 wheel names twenty modes, nine of which have COMPLETION in the name, and the default in the signature of search() is HYBRID_COMPLETION. So unless you pass a type, what comes back is an answer a model wrote and not the text you stored, and CHUNKS is one keyword away. Cognee is also the package most often described as async only, which is true of the memory operations and not of the package: config, the tracing functions and start_ui are ordinary synchronous definitions, a run_sync wrapper ships at cognee.infrastructure.utils.run_sync, and cognee-cli is registered as a console script by the install itself.
Letta's score is real and it is easy to look for in the wrong file. There are two files named passage_search_response.py in the shipped client. The one under letta_client/types/ declares score: float with the docstring "Relevance score"; the one under letta_client/types/agents/ has no score field at all. So the score lives on the non-agent search endpoint, and a reader who greps for the filename alone will find the other one first. As with Mem0 and Zep, our sweep did not capture a changelog for Letta, so no claim here about Letta rests on one and no absence is claimed about that surface. What can be said from PyPI on 8 September 2026: letta-client 1.12.1 is the current stable release; letta 0.16.8 is not yanked, and a dry run of pip install letta==0.16.8 downloads the wheel and reports that it would install; letta-evals 0.26.0 shipped on 27 August 2026 and installs; and the names letta, letta-client, letta-nightly and letta-evals all resolve on PyPI today. The Letta repository README states where the old server went: "The archive branch contains the retired Letta V1 API server."
That is ten different answers behind one word, from eleven rows: stored text, extracted facts, passages, a context block, graph edges, typed objects, ranked memories or chunks, your own JSON, chat messages and files, with LangMem handing back nothing directly. If you are choosing on a feature list you will not see that difference, and it is the one that decides how much code you write after the install.
Three checks, four minutes
If you are choosing between these, three checks separate them and all three are faster than reading a front page.
- Read the runtime dependency list on PyPI, with the extras lines dropped, and read it for contents rather than for length. A store, an index, a database driver or a model client means the memory engine ships in the package, even where it still calls a hosted model or a database server you run. Their absence means the package builds HTTP requests and the memory lives in a server outside it.
- Read the class names the package exports. Where one package ships two products, the import line is the switch, and at least one vendor here publishes the routing itself.
- Construct the client with nothing and read what comes back. Most of the clients raise and name what they ask for first, which is not always everything they need: Graphiti's asks for a database address or a driver, and its default path wants an OpenAI key after that. Two construct silently, and then the question to ask is what address the object is already pointed at. Where a package has no client class to construct, its first call is where the terms appear.
What those three checks do not tell you is what a read returns, and that is the one you have to look up. It is also the column where these stop being the same kind of thing.
One more reason to run them yourself rather than trust this table. Every version number above was true on 8 September 2026 and at least one of them will be wrong by the time you read this. requires_dist will still be there. The class names will still be there. The constructor will still refuse or still construct. The procedure outlives the numbers, which is why it is the part of this article we would defend in six months.
And ours is in the table: hosted only, key required, in the release read here more strictly than anyone else's, and with two things we published that did not survive the check. That is a reason to check us the same way. It is not a reason to buy anything.
Related
- Memory response shape: what a read returns, treated on its own and at length
- Best persistent memory APIs compared: the same vendors on the service side rather than the package side
- MCP memory servers for Claude Code and Cursor: the other door into most of these products, and why the client decides more than the vendor does
- Memory MCP servers compared: the protocol surface of this field
- Agent memory is not a database: what the write path buys you, argued from the other direction
- Python SDK reference: our own package, documented by us
- Best AI agent memory in 2026: our own roundup of the same field; read it against this page rather than instead of it
Every package fact on this page was read on 8 September 2026 from the artifact or the page named beside it, except where a later date is named beside the fact, and except facts about 0.3.0, which was uploaded on 16 September 2026 and read after that: PyPI JSON metadata fetched with full browser headers, wheels downloaded from files.pythonhosted.org and unpacked, and constructors executed in a clean virtual environment on Python 3.11.9 with the relevant environment variables cleared first. Every fact changed on 13 September 2026 was read again that day from the vendor page, the shipped wheel or the live request; where a cell cites "the vendor" without naming a page, the quotation is from that vendor's current published documentation and the page is not pinned here; the constructor table is the 8 September run. Dependency counts exclude every requirement line carrying an extra == marker and keep platform markers, and the counts with extras included are given beside the table so that either number can be reproduced. Every quotation is a contiguous substring of the extracted text of the page or the file named with it, with the vendor's own punctuation, grammar and spelling preserved, including the one grammatical slip in Zep's constructor message. Where this article states an absence it names the surface that was read. A six surface sweep of the six memory vendors was taken on 7 September 2026 and three of the six came back complete; the changelog surface did not resolve for Mem0, Letta or Zep, which is said in each section that makes a claim about those three, and no absence claim on this page rests on a changelog. Disclosure: this site publishes one of the packages compared here, its row is read first and read hardest, and the two things of ours that did not survive the check are named in the second section rather than the last. No performance, latency, accuracy or benchmark number appears in this article, for any system, and no star counts or download counts either.
Edward Izgorodin · Mnemoverse · 2026-09-08
Mnemoverse is a persistent-memory API for AI agents. Free key: console.mnemoverse.com · Plans and limits · Docs: Getting Started
