Python SDK for agent memory, compared: eleven packages, twelve rows
This is the written half of a video of the same name, thirteen minutes in eight chapters, and the sections below follow those chapters in order so that anyone arriving from the end card can pick up where they stopped.
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. 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 whose computer the memory runs on, and it is on the same page as 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, an index or a model.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 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 the exception. Five of these refuse with the vendor's real terms in the message, and the terms are not the same terms. Two construct silently, and one of those two is already pointed at a paid host.
- The trade is not a key against no key. It is: pay a vendor to run the model and the index, or run them yourself. Persisting with no vendor credential is documented, on cookbook pages rather than front pages, and 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. Six different objects behind one word.
- 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 one line we publish about ourselves is still wrong while you read this.
- Version numbers date faster than anything else here. Every version below was re-read from PyPI on 8 September 2026, 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 about our own package did not survive that check.
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 one is still live while you read this, and it is not on this package's repository, which is clean. It is on the memory server we also publish. The one line GitHub prints for github.com/mnemoverse/mcp-memory-server, in the page title and in the meta description, still carries two claims our own documentation has withdrawn: that the memory forgets, and that recall thins with age. The withdrawal is explicit on our side: nothing about a stored memory decays over time or is deleted by age. The README was corrected and the npm description was corrected. The repository description was not, and it is the first line a stranger reads. Checked live on 8 September 2026. The withdrawn sentence is described here rather than reprinted, 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.
One correction to the video, and it is about us
The video says our client raises on a precondition rather than on a key. It does not, and the true version is stricter than the one we said.
Constructed with nothing, from the shipped wheel, MnemoClient() raises a plain TypeError: MnemoClient.__init__() missing 1 required positional argument: 'api_key', and AsyncMnemoClient() raises the same with its own name. The string precondition does not occur in any of the six modules of the package; the four error classes it defines are MnemoError, MnemoAuthError, MnemoRateLimitError and MnemoUnavailableError. So the refusal is not a message we wrote at all. It is the language refusing, because the requirement is expressed in the signature, and there is no environment fallback anywhere in the package to soften it.
One sentence in that chapter of the video 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 install line is the least informative thing here
Twelve rows, eleven packages, and twelve 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 | whose computer the memory runs on |
|---|---|---|---|---|
mnemoverse (ours) | 0.2.0 | 2026-08-14 | 2 | ours, hosted only |
mem0ai, the Memory class | 2.0.20 | 2026-09-02 | 8 | yours, in your process |
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 |
graphiti-core | 0.30.1 | 2026-09-01 | 7 | yours, plus a graph database you provide |
cognee | 1.5.4 | 2026-09-04 | 45 declared lines, 44 distinct names | yours, with the stores embedded |
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 that describes what a bare pip install puts on your disk, and it is the number in the table.
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 already told you
requires_dist is published by every project on PyPI, it is one click from 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 and no model in its dependency list is a request builder. It assembles HTTP requests, and the memory runs on somebody else's computer.
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 packages run memory inside your process, with a different failure mode, a different bill and a different thing to operate, and you could tell from a page you were already on.
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 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 a paid hosted service or stays on your laptop. 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 company. 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, it is a separate package, and it expects an external graph database that you provide. The same sweep did not capture a changelog for Zep either, so the same boundary applies to every Zep and Graphiti claim here.
Graphiti is also the one package here that gives you no choice about how you call it. 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. Every other client in this comparison ships both, usually 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 whose computer it runs on and which of a vendor's products you are about to get.
The constructor is where the terms are stated
The third check is not published as documentation at all. 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 client object to construct; the key requirement appears when an operation calls the model |
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 refusal is not about an account at all. It asks for a database address, because without one there is nowhere to put anything. That single line answers the question this article says to ask first, and it answers it before the question of who bills you.
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 is the strictest of the eleven, and not because we wrote a better message. api_key is a positional parameter with no default in both clients, and a full grep of the shipped wheel finds no getenv, no environ and no dotenv anywhere in it; running the constructor with MNEMOVERSE_API_KEY and MNEMO_API_KEY both set still fails. Mem0, Zep and Supermemory all read an environment variable before they complain. We do not read one at all. That is stricter, and it is also less helpful: what you get is the language's own error rather than a sentence explaining anything.
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. 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, because a constructor cannot round its own requirements down.
What it costs to persist, and the page nobody leads with
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. It is: pay a vendor to run the model and the index, or run them yourself. Every hosted service here asks for a key and says so in the constructor, before you have written a second line. That much is uniform. But persisting with no vendor credential is documented, on cookbook pages rather than front pages, and 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 its ten providers: OPENAI, OLLAMA, CUSTOM, GEMINI, MISTRAL, ANTHROPIC. It does not follow that the other four 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: 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 somebody else's computer. 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 Claude Code 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. No MCP server Mem0 ships 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 a paid product: 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 at all 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, and the two places it is thin
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 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 exist, ever: 0.1.0 on 9 April 2026 and 0.2.0.
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.
Hosted only, and the dependency list proves it rather than the front page asserting it. Both clients issue HTTP requests to https://core.mnemoverse.com, and one method is exempt from the key, the health check, 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 first line of our own source instead of being inferred:
"""Synchronous Mnemoverse client — wraps AsyncMnemoClient with asyncio.run()."""The key is required, and required more strictly than anywhere else in this table. Positional, no default, no environment fallback, in both clients, with every request carrying it as a header. A request without it comes back as a refusal from the server rather than as a helpful local error. As the correction above records, the exception you get from an empty constructor is the language's, not ours.
Now the two thin places, and both are ours to fix rather than anyone else's to forgive.
The first we found and wrote down, and it is fixed. 0.1.0 shipped a default base_url pointing at a host that answers 401, which meant a client configured the ordinary way could not reach the service at all, and 0.1.0 was the only published release for four months. That is in our changelog in our own words, quoted in the disclosure above. It is a historical statement and not a probe you can re-run: that host answers 404 from a different service today, so anyone re-checking will see a different code than the changelog names.
The second is still live as you read this, and it is worse for exactly the reason this comparison exists: it is not a defect in the package, it is a defect in what we publish about the package. The one line description on our public repository still carries a claim our own documentation retracted, in the page title and in the meta description, which means it is read by people and by answer engines before anything else we have written. It has been wrong for longer than it should have been.
This piece argues that what a vendor publishes is checkable. One line of what we publish does not survive the check, and pointing at it here is cheaper than hoping nobody runs the check.
What a read gives back, which is where they actually differ
Storing is the part these products agree on. 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, so not what you put in | the vendor: "By default, Mem0 stores extracted memories, not a verbatim transcript" |
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 | one synthesised context string, built for a system prompt | ThreadContextResponse has one declared field, documented as "Meant to be replaced in the system prompt on every chat turn" |
graphiti-core | ranked graph edges, each with a fact, a validity window and its provenance | EntityEdge declares fact, valid_at, invalid_at, expired_at and episodes |
cognee | typed objects whose text is usually a generated answer | search() defaults to SearchType.HYBRID_COMPLETION |
supermemory | ranked items with a similarity figure and a version graph | similarity: float, documented as "Similarity score between the query and memory entry", plus a context object |
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 is the most opinionated answer in the field, and it is opinionated on purpose. The response type has exactly one declared field, and its docstring 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." That is a product decision about what your prompt should say, shipped as a type. It is a defensible one. It is also not something you undo at read time.
Graphiti's is the opposite decision. You get edges and 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". Provenance and a validity window, 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 retired Letta V1 server source is preserved on the archive branch for historical reference."
That is six genuinely different objects behind one word: stored text, extracted facts, passages, a prompt string, graph edges, and files. 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 work happens on your machine. Their absence means the package builds HTTP requests and the memory lives somewhere else.
- 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 the exception. Five of these state their real terms in that first message. Two construct silently, and then the question to ask is what address the object is already pointed at.
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, strictly, and with one line of what we publish about it wrong on the day this went out. 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
Every package fact on this page was read on 8 September 2026 from the artifact or the page named beside it: 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. 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; the two em dashes on this page sit inside quotations of our own writing, one from our source file and one from our repository description, and neither belongs to a vendor. 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 one line of ours that is still wrong is 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
