You pick a memory package for your agent the way most people do: the landing page has an install command, you paste it, it installs cleanly, you write the first lines from the quickstart, and the first call fails. The error is not about your code. It wants a key, or a database address, or a token limit, or it wants nothing at all and quietly stores your memory in a dictionary that will be gone when the process exits. The install worked. The memory did not arrive with it.
So which Python package actually gives an agent memory after pip install? I put that question to eleven packages, including the one I work on, and the answer is none of them, because the install command is the least informative fact any of these vendors publish. Three other things, all published and none advertised, tell you what you bought, and each is faster to read than a landing page: the runtime dependency list on PyPI, the class names the package exports, and what the constructor asks for first when you hand it nothing.
One line to carry: pip install gives you a client or an engine, never memory, and the dependency list tells you which of the two before you type anything.
Eleven packages, twelve rows
The row count is one higher than the package count because pip install mem0ai is two rows: the classes Memory and MemoryClient come out of the same install and are two different products. Every version and count below was re-read from pypi.org/pypi/<name>/json on 8 September 2026; the versions will date, the checks will not.
| package | version | runtime dependencies, extras excluded | where the memory runs by default |
|---|---|---|---|
mnemoverse (ours) |
0.2.0, and 0.3.0 since 16 September | 2 | ours, hosted only |
mem0ai, the Memory class |
2.0.20 | 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 | 8 | Mem0's hosted platform |
letta-client |
1.12.1 | 6 | Letta's by default, yours if you set base_url
|
zep-cloud |
3.28.0 | 5 | Zep's cloud, or your own cloud through Zep's BYOC offer for enterprise customers |
graphiti-core |
0.30.1 | 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 | 45 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 | 6 | their server, or a binary you run |
langgraph |
1.2.11 | 6 | yours |
langmem |
0.0.30 | 8 | yours, with a model called remotely |
llama-index-core |
0.14.24 | 29 | yours |
deepagents |
0.7.13 | 7 | yours, as files |
The counting rule: every requirement line carrying an extra == marker dropped, platform markers such as sys_platform kept because they are not extras, and that is the number closest to what a bare pip install puts on your disk. 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. And a count is a structural fact rather than a score: forty five lines is not worse than two, it is more work happening on your machine.
Check one: read the dependency list for contents, not length
requires_dist is published by every project on PyPI, in the JSON the index serves at pypi.org/pypi/<name>/json, and it settles the largest question here before you install 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 httpx and pydantic. Zep's cloud package declares five. Letta's client and Supermemory's client declare six each, and the two lists are identical name for name, which tells you both came 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 declare something quite different. graphiti-core lists neo4j and openai: a graph database driver and a model client. mem0ai lists qdrant-client and sqlalchemy: a vector store client and a database toolkit. cognee lists lancedb, an embedded vector store, ladybug, a graph database declared twice under mutually exclusive platform markers, and fastapi, starlette, uvicorn and gunicorn, which is a web server. Those three ship the memory engine in the package, which is not the same as running entirely in your process: each calls a hosted model by default, Graphiti wants a Neo4j or FalkorDB server you run unless you install an embedded graph engine behind a pip extra, 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.
Check two: the import line, when one package is two products
mem0/__init__.py in the shipped 2.0.20 wheel does all of its public routing in two import lines: AsyncMemoryClient and MemoryClient from mem0.client.main, AsyncMemory and Memory from mem0.memory.main. Four names, two products, and the vendor states the routing itself in the index it publishes for answer engines, 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; the vendor's own overview says "Mem0 Open Source is the same memory engine as the Platform, running on your own infrastructure."
The reverse case is the pair that gets quoted as though it were one product: zep-cloud is the hosted client with a single hosted address in its environment enum, and Graphiti is the engine in a separate package, which Zep's own FAQ calls "The open-source Context Graph framework that powers Zep Cloud". graphiti_core exposes exactly one class, Graphiti, with no synchronous twin, and the memory functions of cognee are async as well, while the Mem0, Letta, Zep, Supermemory and Mnemoverse clients each ship both, as a pair of names differing by the word async.
Check three: construct the client with nothing and read what comes back
This check needs a terminal rather than a web page. I ran every constructor 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.
-
MnemoClient(), ours, on 0.2.0:TypeError: MnemoClient.__init__() missing 1 required positional argument: 'api_key'; on 0.3.0, released on 16 September and re-run on 17 September, the same call readsMNEMOVERSE_API_KEYand, with it unset, raisesValueError: No API key: pass api_key= or set the MNEMOVERSE_API_KEY environment variable -
MemoryClient(), Mem0 hosted:ValueErrorsaying the Mem0 API key was not provided -
Memory(), Mem0 open source:openai.OpenAIErrorabout missing credentials, because on its default path Mem0 calls a model during ingestion -
Zep():ApiErrorasking forapi_keyorZEP_API_KEY -
Supermemory():SupermemoryErrorasking forapi_keyorSUPERMEMORY_API_KEY -
Graphiti():ValueErrorsayingurimust be provided when no graph driver is given -
Letta(): constructs, no exception -
InMemoryStore(), LangGraph: constructs, no exception -
Memory(), LlamaIndex:ValidationErroron an unset token limit, no credential involved -
cognee,langmem,deepagents: no client class to construct
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; once a driver is given the address is not needed, because the embedded Kuzu driver's database path defaults to an in-memory one. And the two silences are terms as well. Letta() constructs with nothing because its environment table names a cloud address first and localhost second, so the silent object is already pointed at a hosted service and the local address is one keyword argument away. InMemoryStore() constructs with nothing because there is nothing to authenticate against: it is a dictionary.
The trade is not a key against no key
The tidy version people repeat is that everything which survives a restart needs a key, and everything which needs no key does not survive a restart. The vendors' own pages refute it. The trade is also not one decision: the model and the index are placed separately. Graphiti's default path runs the graph on your machine and still wants an OpenAI key, while Mem0's local recipe puts both the model and the index on yours. Persisting with no vendor credential is documented by Mem0, Cognee, Graphiti and Letta, and none of it 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)".
Mem0 publishes a recipe for exactly this, headed Self-Hosted AI Companion: "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 and an Ollama. Cognee's floor is lower because its stores are already inside the package, and its local setup page is the sharpest statement of the trade in this field, cutting both ways in one paragraph. First: "running it locally with Ollama needs no account, subscription, or paid API key", and "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 sentence will file a bug.
The same applies one door over: an MCP server does not mean your computer. Mem0's hosted one says so in the vendor's words: "Nothing runs on your machine: the server is hosted by Mem0, and your client connects to it over HTTPS." Cognee's standalone server runs the whole pipeline locally. Same protocol, opposite answers.
And the two rows that need no credential out of the box are the two that do not persist, and both vendors say so first. The comment in LangGraph's own code sample reads "InMemoryStore saves data to an in-memory dictionary. Use a DB-backed store in production use." LlamaIndex's page says "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 row, and I read it hardest
I work on mnemoverse, so weigh this section accordingly. It is a hosted client. The dependency list shows that no memory engine ships in the package: two runtime dependencies, httpx and pydantic, in a pure Python wheel of 13,468 bytes for 0.2.0, with both clients issuing HTTP requests to our service. That the service behind it is hosted only is my own statement, which no dependency list can show. In 0.2.0, the release this table read on 8 September, the key was required more strictly than anywhere else in the table: api_key was positional with no default in both clients, a full grep of that wheel finds no getenv, no environ and no dotenv, and what you got was the language's own error rather than a sentence. 0.3.0, uploaded to PyPI on 16 September 2026, changed that, and our changelog says so: api_key is now optional, the key is read from the MNEMOVERSE_API_KEY environment variable when it is not passed, and a constructor given neither raises ValueError: No API key: pass api_key= or set the MNEMOVERSE_API_KEY environment variable. On the current release we behave the way Mem0, Zep and Supermemory do, reading an environment variable before we complain.
Two things we published did not survive the check this article recommends: one in the package, one about the memory server we also publish. The first we found ourselves and wrote into our changelog: 0.1.0 shipped a default address pointing at a host that answered 401, so a client configured the ordinary way could not reach the service at all, and it was the only published release for four months until 0.2.0 corrected the default. The second was not in this package. It was the one line description of the memory server we also publish, mcp-memory-server, which GitHub prints in the page title and the meta description: when the full write-up went out on 8 September 2026 it 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.
Four minutes, then decide
What the three checks do not tell you is what a read hands back, and that is where these stop being one category: ranked stored text, extracted fact strings with a score by default (Mem0 says so itself: "By default, Mem0 stores extracted memories, not a verbatim transcript", and documents a flag that stores content exactly as provided), passages, a context block beside a search that returns ranked edges, graph edges where some carry a validity window, ranked memories or document chunks, an answer a model wrote, your own JSON, chat messages, or files. That column decides how much code you write after the install, and it is the one you have to look up.
Read requires_dist with the extras dropped, for contents rather than length. Read the exported class names, because where one package ships two products the import line is the switch. Construct the client with nothing and read what comes back, knowing that a first refusal names what the client asks for first and not always everything it needs, and where it constructs silently, ask what address the object is already pointed at. At least one version number above will be wrong by the time you read this, ours included, since 0.3.0 shipped on 16 September, and all three checks will still work. The mnemoverse row sits in the table on the same terms: hosted only, a key required on every release, and two things we published that did not survive the check. That is a reason to run the checks on it too, and not a reason to buy anything.
Which of the three checks would have changed the package you already installed?
Every quotation above is a contiguous substring of a page the vendor published, re-checked on 2026-09-17, and the full write-up carries the same quotations with the method behind them.
Disclosure: I work on Mnemoverse, one of the systems above, so weigh the argument accordingly. The full comparison with every source page is on our library.
Top comments (2)
Really interesting perspective.
From an architecture standpoint, the part that stood out to me is the difference between installing a library and actually getting the capability behind it.
It’s similar to using a database or messaging SDK: the package is just the interface. What really matters is where the state lives and what you now have to operate.
That makes me see agent memory less as a library feature and more as an architectural capability with its own lifecycle and operational concerns.
That is the reading I would keep, with one refinement from going through the eleven. What you now have to operate is usually two things placed separately, not one: where the store runs and where the model that runs on write is called. Several of the local defaults keep the index on your machine and still call a hosted model on write, so a laptop install can carry an API bill and a network dependency without looking like a service.
The lifecycle part shows up at upgrade time more than at install time. With a hosted client, upgrading the package changes the interface and leaves the data where it was. With a store you run, the data is yours to migrate, and the quiet case is the embedding model: change it and every stored vector was computed by a model you no longer call, so the index has to be rebuilt before recall means the same thing again. The dependency list tells you which of the two situations you are signing up for before you install anything.