Skip to content

How can I implement long-term associative memory in Python for autonomous agents?

TL;DR

  • Test associative memory by asking whether a read returns something the query never named, reached through a stored relationship.
  • The Python example below implements that behaviour without a key or service. Its verified run shows an unnamed note entering the results through a learned link.
  • Compare the mechanism, not the label. Vendor documentation describes graph memory, graph completion, traversal, and semantic archival search; these do not promise identical behaviour.
  • In Mnemoverse, association expansion is on by default. Inspect query_concepts, expanded_concepts, and per-item source, then use feedback to teach future reads.

Here is one line from the program further down this page. The query asked about timeout, and this note came back:

text
   - Backoff of two seconds fixed the flaky call. [NEVER ASKED FOR, arrived through retry]

The note never mentions a timeout. It came back because it was recorded as belonging with something the query did name.

Associative memory is a store whose read returns items the query never named, because those items were recorded as belonging with something the query did name.

That is the working definition for this article. It gives you a test before you choose an implementation.

Write a note about a problem and a remedy. Write another about that remedy and a useful adjustment. Ask about the original problem. Does the adjustment return, even though neither its text nor its concepts names the problem? More importantly, can you establish that the relationship brought it back?

The Python program below makes that distinction visible. It also separates three decisions worth keeping separate in production: what enters memory, what a read follows, and what feedback changes.

Disclosure: We build Mnemoverse, one of the products compared here. Vendor documentation was read on 2026-09-20 and independently reviewed on 2026-09-21. Mnemoverse API facts and the explicitly labelled live reads were verified on 2026-09-21.

Why similarity search alone does not establish associative memory

Similarity search is retrieval that ranks stored material by its similarity to a query. Letta’s archival-search reference describes its query as a string searched using semantic similarity, providing a concrete example of that contract. Source: Letta archival search.

A result need not repeat the query’s words to be a semantic match. That makes “it returned an unnamed item” a useful starting test, but not sufficient evidence of a stored association. Look for the retrieval path as well as the result.

The vendor mechanisms differ:

  • Cognee’s graph-completion guide distinguishes a fragment restricted to vector-scored node IDs from retrieval that reaches surrounding context through neighborhood_depth. Source.
  • Mem0 Platform describes connected memories receiving a ranking boost through Graph Memory. Source.
  • Supermemory exposes related memories through an option on its search request. Source.
  • Zep’s default Context Block already combines semantic search, full-text search, and breadth-first graph traversal. Graph expansion is not something every Zep user must separately enable. Source.

Do not assume these implementations share one execution order. Instead, distinguish matching the query from following relationships around the matches.

For design context, knowledge graphs versus retrieval examines the difference between asserted relationships and learned associations. Here, the practical question is narrower: which relationship changed this read?

A standard-library Python associative memory example

The example implements the behaviour in 109 lines using json, os, and defaultdict from collections. Save it as assoc_min.py in an empty directory and run python assoc_min.py.

We ran it twice on 2026-09-21, with Python 3.14.3 in an empty directory. It needed no installed package, API key or service, and both runs printed the output below byte for byte.

The implementation stores notes with concepts. Writing concepts together increases a pair counter. Reading starts with the requested concepts, finds sufficiently strong neighbours, and scores notes using both requested and expanded concepts.

python
"""Long-term associative memory for an agent, in plain Python.

Chapter 2 of the film is this file and nothing else: standard library only, no vendor,
no key, no server. It exists to make one behaviour concrete, the behaviour the whole
category is named after: a read comes back with something the query never named.

Three parts, and that is the whole idea.
  1. Items are stored with the concepts they are about.
  2. Concepts that appear together get a link, and the link thickens each time.
  3. A read starts from the query's concepts, walks one step along the strongest links,
     and scores items on both what was asked and what the links added.

Run it: python assoc_min.py
"""
import json
import os
from collections import defaultdict

STORE = "memory.json"


def load():
    if os.path.exists(STORE):
        with open(STORE, encoding="utf-8") as f:
            d = json.load(f)
        return d["items"], defaultdict(int, {tuple(k.split("|")): v for k, v in d["links"].items()})
    return [], defaultdict(int)


def save(items, links):
    with open(STORE, "w", encoding="utf-8") as f:
        json.dump({"items": items, "links": {"|".join(k): v for k, v in links.items()}}, f, indent=1)


def write(items, links, text, concepts):
    """Store one memory, and thicken the link between every pair of concepts in it."""
    items.append({"text": text, "concepts": sorted(concepts)})
    for a in concepts:
        for b in concepts:
            if a < b:
                links[(a, b)] += 1
    return items[-1]


def related(links, concept, limit=3, floor=2):
    """The concepts most often recalled together with this one. One step, strongest first."""
    near = [(other, n) for (a, b), n in links.items() for other in [b if a == concept else a if b == concept else None] if other and n >= floor]
    return [c for c, _ in sorted(near, key=lambda p: -p[1])[:limit]]


def read(items, links, query_concepts, top_k=3):
    """Score every item on the query's own concepts plus the concepts the links add."""
    expanded = {}
    for c in query_concepts:
        for r in related(links, c):
            if r not in query_concepts:
                expanded[r] = max(expanded.get(r, 0), links[tuple(sorted((c, r)))])
    hits = []
    for it in items:
        direct = len(set(it["concepts"]) & set(query_concepts))
        added = sum(w for c, w in expanded.items() if c in it["concepts"])
        if direct or added:
            hits.append((direct * 10 + added, direct, it))
    hits.sort(key=lambda h: -h[0])
    return [{"text": h[2]["text"], "asked_for": h[1] > 0, "via": sorted(set(h[2]["concepts"]) & set(expanded))} for h in hits[:top_k]], sorted(expanded)


def feedback(links, query_concepts, result_concepts, helped=True):
    """The read that helped thickens the links between what was asked and what came back."""
    step = 1 if helped else -1
    for a in query_concepts:
        for b in result_concepts:
            if a != b:
                links[tuple(sorted((a, b)))] = max(0, links[tuple(sorted((a, b)))] + step)


def show(items, links, query, label):
    answer, expanded = read(items, links, query, top_k=4)
    print(label)
    near = ", ".join("%s(%d)" % (c, links[tuple(sorted((query[0], c)))]) for c in expanded) or "nothing yet"
    print("   asked:", ", ".join(query), "| the links add:", near)
    for row in answer:
        how = "asked for" if row["asked_for"] else "NEVER ASKED FOR, arrived through " + ", ".join(row["via"])
        print("   -", row["text"], "[" + how + "]")


if __name__ == "__main__":
    items, links = [], defaultdict(int)

    # Monday. Three things happen, and each is written down with what it is about.
    write(items, links, "The deploy timed out after the release.", ["timeout", "deploy"])
    write(items, links, "Retried the request and it went through.", ["timeout", "retry"])
    write(items, links, "Backoff of two seconds fixed the flaky call.", ["retry", "backoff"])
    show(items, links, ["timeout"], "MONDAY, first read")

    # Tuesday and Wednesday. The same two things keep happening together.
    write(items, links, "Timed out again, retried, fine.", ["timeout", "retry"])
    write(items, links, "Another flaky call, retry with backoff.", ["retry", "backoff"])
    show(items, links, ["timeout"], "WEDNESDAY, the same read, nothing else changed")

    # The agent says the answer helped. The path that produced it gets easier to walk, and
    # the second time it is walked, a concept the query never named becomes a neighbour of its own.
    feedback(links, ["timeout"], ["retry", "backoff"])
    show(items, links, ["timeout"], "AFTER ONE FEEDBACK CALL, the same read again")
    feedback(links, ["timeout"], ["retry", "backoff"])
    show(items, links, ["timeout"], "AFTER THE SECOND, the same read again")

    save(items, links)
    print("stored in", STORE + ", and the next process starts with these links, not from zero")

The verified run

This is the output of that run, unedited.

text
MONDAY, first read
   asked: timeout | the links add: nothing yet
   - The deploy timed out after the release. [asked for]
   - Retried the request and it went through. [asked for]
WEDNESDAY, the same read, nothing else changed
   asked: timeout | the links add: retry(2)
   - Retried the request and it went through. [asked for]
   - Timed out again, retried, fine. [asked for]
   - The deploy timed out after the release. [asked for]
   - Backoff of two seconds fixed the flaky call. [NEVER ASKED FOR, arrived through retry]
AFTER ONE FEEDBACK CALL, the same read again
   asked: timeout | the links add: retry(3)
   - Retried the request and it went through. [asked for]
   - Timed out again, retried, fine. [asked for]
   - The deploy timed out after the release. [asked for]
   - Backoff of two seconds fixed the flaky call. [NEVER ASKED FOR, arrived through retry]
AFTER THE SECOND, the same read again
   asked: timeout | the links add: backoff(2), retry(4)
   - Retried the request and it went through. [asked for]
   - Timed out again, retried, fine. [asked for]
   - The deploy timed out after the release. [asked for]
   - Backoff of two seconds fixed the flaky call. [NEVER ASKED FOR, arrived through backoff, retry]
stored in memory.json, and the next process starts with these links, not from zero

What the implementation does

Read the program as a sequence of responsibilities, rather than a miniature product.

write records evidence of association. It appends a note with sorted concepts and increments the counter for each concept pair inside that note. A link initially means only that its concepts were written together.

related chooses which links matter. It considers neighbours whose pair count reaches floor, sorts them by weight, and limits the returned neighbours. The example defaults to floor=2 and limit=3. These are demonstration settings, not measured recommendations.

read separates direct and expanded matches. It builds expanded concepts from the neighbours, scores every note, and drops notes that match neither set. Its returned rows carry asked_for and, for association-only results, via.

feedback changes relationships using the outcome. It increases links between requested and returned concepts when a result helped. With helped=False, it decreases those weights without taking them below zero.

save and load preserve both notes and links. They write and read the whole memory.json file. Calling load in a later process restores the relationships as well as the content.

Two constants carry the behaviour. floor=2 is why a pair written once widens nothing: one co-occurrence can be chance, so the example waits for a second before it follows the link. The ten in direct * 10 + added keeps a note you asked for ahead of one the links brought in, until the weights of the links reaching that note add up to more than ten.

These details are inspectable in the program above. None depends on an embedding model or a remote service.

What to notice in the output

The Monday read establishes the baseline. Notes have been written, but the relevant concept pairs have not crossed the expansion threshold.

Later writes repeat the relationships. The query stays the same, yet a note about backoff enters the results through retry. That note does not carry the requested timeout concept. Its retrieval explanation names the relationship that brought it in.

Feedback then changes the path. It strengthens existing associations, and after the second call backoff is a direct neighbour of the query concept. Watch the expansion explanation, not just the result order: learning can change why an item returns without changing which items return.

The demo starts from an empty store on purpose. It saves the resulting store but does not load a previous run at startup. That makes its printed sequence repeatable. To test persistence rather than repeatability, call load from a later process and inspect the saved links.

Where the example stops

The file makes its shortcuts visible. Concepts are caller-written strings. Matching uses exact concept equality, so wording variants are distinct unless you normalise them. Ranking uses concept counts and link weights rather than semantic relevance.

The program scans the notes and rewrites one JSON file on save. It provides no indexed retrieval or coordination between concurrent writers. It also performs no independent review of stored content: changes happen when you call its functions.

Those are properties of this example, visible in its implementation. Use it to learn and test the behaviour. Treat the production requirements as a separate design task.

What production associative memory needs

For an autonomous agent, set these acceptance criteria before choosing a package:

  1. Persistence of relationships. Restart the process and repeat the query. Require the stored associations, not only the notes, to survive.
  2. Learning from useful results. Record outcomes after retrieval. Require evidence that feedback changes a later read, rather than treating every co-occurrence as equally useful.
  3. Storage beyond the demonstration file. Evaluate indexed retrieval and coordinated access against your workload. The whole-file implementation above is a baseline to replace, not a capacity claim.
  4. Shared access across working tools. Decide which agent, application, and developer tools must reach the same memory. Test that identity boundary explicitly.

These are proposed engineering checks, not claims that the compared products satisfy identical requirements. The Python SDK comparison provides another view of the integration boundary.

The word you typed: “associative”

Our documentation sweep found zero exact occurrences of associative on the following documentation surfaces. This is a dated terminology measurement, not proof that a product does or does not implement a behaviour.

  • Mem0: the documentation export covering 244 pages. The product documentation calls the mechanism Graph Memory and describes multi-hop recall. Documentation export, read 2026-09-20.
  • Cognee: all 402 addresses in its documentation sitemap, plus the documentation export and core shard. Its terms include knowledge graph, triplets, memory fragment, and graph completion. Sitemap, documentation export and core shard, read 2026-09-20 and 2026-09-21.
  • Zep: 337 documentation URLs. Relevant terms include breadth-first search, node distance, and episode mentions. Documentation sitemap, read 2026-09-20.
  • Letta: the 589-file vendor Markdown mirror and 20 live documentation pages. The documented archival read uses semantic search. Vendor mirror and search reference, read 2026-09-20.
  • Supermemory: 146 documentation pages plus the live API specification. Its terms include graph memory and memory graph traversal. Documentation index and live specification, read 2026-09-20.

The Letta forum check also returned zero exact matches. Its stemmed search returned “associated”, which is not an occurrence of “associative”. That correction was verified on 2026-09-21 against the forum.

Mem0’s blog is an important exception. The review found the word in three posts among the 208 blog addresses in its sitemap. One describes Mem0’s own APIs as supporting:

“including episodic, semantic, procedural, and associative memories”

That is the vendor’s wording, not an interpretation of its feature set. Source: Mem0’s persistent-memory API article, read 2026-09-21.

The useful conclusion is a search strategy: look for each vendor’s mechanism name as well as the buyer’s term.

Associative memory APIs: the production map

The table uses vendor-documented calls and mechanisms, not an overall ranking. Sources were read on 2026-09-20, with the corrections below verified on 2026-09-21. Each mechanism cell links its supporting documentation.

ProductWrite callRead callWhat widens the readWhat reorganises later
Mem0Hosted client.add(...); library m.add(...). Platform, libraryHosted client.search(...); library m.search(query, filters={...}). Library quickstartPlatform Graph Memory boosts memories connected to matched entities. The open-source SDK instead documents entity-overlap retrieval without graph memory. Graph Memory, retrievalPlatform Dream automatically handles supersession (marking the older fact outdated) and merging; synthesis has separate availability. Dream
Cogneeawait cognee.remember(text). Quickstartawait cognee.recall(query_text="..."); without an explicit type, recall selects a retrieval strategy. RecallGraph completion returns triplets among the vector-matched nodes; neighborhood_depth brings in their connections. GuideImprove adds derived retrieval structures. Permanent writes, as well as session writes, trigger a follow-up pass by default. Remember
Zepclient.graph.add(...); conversations use client.thread.add_messages(...). Adding messagesclient.graph.search(...); the Context Block derives its query from recent thread messages. Search, quickstartThe default Context Block combines semantic search, full-text search, and breadth-first graph traversal. Context BlockGraph creation merges duplicates and invalidates contradicted facts. Observations derive higher-level patterns. Graph creation, Observations
Lettaclient.agents.passages.create(agent_id=..., text=...). The cited Python reference is marked deprecated. Python referenceclient.agents.passages.search(agent_id=..., query=...). SearchNo relationship-expanding read was documented in the reviewed surfaces. The archival call documents semantic search. Search referenceDreaming uses background subagents to review conversations and update memory. Memory configuration
Supermemoryclient.add(content=..., container_tag=...). SDKclient.search.memories(q=..., container_tag=..., include={"relatedMemories": True}). QuickstartGraph memory returns related memories and version context around hits. Search referenceDreaming continues extracting facts, linking memories, and resolving updates after indexing. Graph memory
Mnemoverseclient.write(content, concepts=[...]). Python SDKclient.read(query). Python SDKHebbian expansion follows learned associations by default. Responses expose expanded_concepts and per-item source. Referenceclient.feedback(atom_ids, outcome, query_concepts=...) updates valence and associations, teaching subsequent reads. Getting started

Two qualifications matter when using this map.

First, Letta’s row is a scoped documentation finding. It covers the live documentation pages, vendor mirror, and forum reviewed for this article, not Letta source code. Its Constitution describes tightening pathways to related context as an agent principle. That is different from a documented retrieval call. Letta’s Python client calls a server; the table does not infer an undocumented linking mechanism.

Second, later reorganisation is not one uniform operation. Cognee adds retrieval structures; Zep handles contradictions during graph creation; Mnemoverse updates associations from feedback. Match the operation to the change you need to observe. The broader memory knowledge-graph comparison offers related context.

Mnemoverse: associative memory as an API

Mnemoverse is a persistent-memory engine and API for agents to store, retrieve, and verify knowledge across sessions. Its Python interface exposes the associative loop through write, read, and feedback. Sources: Python SDK, API reference.

Install with pip install "mnemoverse>=0.3.0", then import MnemoClient from mnemoverse. The SDK supports Python 3.10+ and accepts a key through MNEMOVERSE_API_KEY or api_key=. Source: SDK documentation, verified 2026-09-21.

The calls have distinct responsibilities:

  • Write: client.write(content, concepts=[...]) stores content with concepts described in the reference as “Key concepts for Hebbian associations”.
  • Read: client.read(query) performs semantic search with Hebbian expansion. include_associations defaults to True in the client signature and to true in the REST reference.
  • Feedback: client.feedback(atom_ids, outcome, query_concepts=...) updates valence and Hebbian associations.

Hebbian associations are weighted links between concepts, learned when concepts occur together as memories are stored and recalled, with feedback tuning the weights, after the old rule that cells which fire together wire together (more on the mechanism). Valence is each memory's outcome score, from -1 for failed to +1 for successful, which feedback moves.

These signatures and defaults are documented in the API reference.

Inspect the expansion, not just the answer

A read exposes query_concepts, the concepts extracted from the query, and expanded_concepts, the concepts after Hebbian expansion.

Each result also carries source, naming the stage that found it. The values in our reads were:

  • semantic: found by semantic retrieval.
  • concept_scoped: found under a concept extracted from the query.
  • hebbian_retrieval: missed by the direct stages and reached through learned associations, via a concept the query did not name.

That last field connects the behavioural test to a response you can inspect. A longer result list alone is not the evidence; its retrieval-stage labels are. Source: response reference, verified 2026-09-21.

Our own run, 2026-09-21. We made three live reads on our own account, each asking for top_k=10. They returned 15, 11 and 15 items. In each read, ten items came from the direct stages (semantic and concept_scoped), and the association layer added 5, 1 and 5 more, labelled hebbian_retrieval. The expanded_concepts lists held 20, 42 and 20 concepts.

The counts depend on what an account holds. The reference documents this behaviour for top_k: "Not a hard cap: association expansion can return more, and the relevance floor can return fewer". The direct results fill the request, and what the associations find arrives on top of them.

Close the learning loop

The getting-started guide describes feedback as creating co-activation links between query concepts and result concepts. Helpful outcomes strengthen those relationships for later reads, including reads in another session. The Hebbian-memory article explains the mechanism.

Signed in to the same Mnemoverse account, Claude, Cursor and VS Code reach this memory through MCP, and an application using Python reads and writes the same store. ChatGPT connects to it too; the ChatGPT page lists what each ChatGPT plan allows. Source: MCP server. For sharing between accounts, rooms provide shared memory in Beta.

Three checks for your existing agent memory

Run these checks with a small, controlled set of notes before judging a memory system from an agent’s final prose answer.

Check 1: Does a read return what the query never named?

Write a note connecting X to Y, then another connecting Y to Z without naming X. Query X and inspect whether the second note returns through the relationship.

Use the implementation’s documented graph or expansion settings. The example has a co-occurrence threshold; do not assume another product uses the same learning rule.

Check 2: Can you see what the expansion added?

Inspect the response for added concepts, related-memory objects, or retrieval-stage labels. Where comparison settings exist, hold the query and stored content fixed.

A changed ordering is evidence that retrieval changed. It does not, by itself, identify which relationship caused the change. Prefer an explanation attached to the result.

Check 3: Does anything change the memory between sessions?

Separate persistence from learning. First restart and confirm the same memory survives. Then submit feedback or a correction, allow the documented operation to complete, and repeat the query in a new session.

Do not require all implementations to change for the same reason. Record whether the observed change came from outcome feedback, write-time processing, or a background pass.

The table below gives each product an evidence target for each check. It is a reading guide, not a set of claimed cross-vendor test results.

ProductCheck 1: unnamed resultCheck 2: visible expansionCheck 3: change across sessions
Mem0Test Platform Graph Memory’s connected-entity retrieval. SourceThe graph affects ranking without adding a separate graph response payload. Do not infer per-item association provenance from that response alone. SourceInspect Dream activity and changed memory after its documented operations. Source
CogneeExercise graph completion with neighbourhood context. SourceInspect returned triplets; distinguish vector-scored fragments from wider neighbourhood context. SourceInspect retrieval after the default follow-up Improve pass on a permanent or session write. Source
ZepTest the default Context Block, which already includes graph traversal. SourceInspect graph-search results and documented reranking settings; do not equate configuration with a per-item expansion label. SourceWrite a correction and inspect how graph creation invalidates the older fact. Source
LettaThe reviewed archival call documents semantic search, not linked-memory expansion. SourceNo association-expansion payload is documented for that call. A desktop memory-file graph viewer is a different facility. SourceTest memory updates from Dreaming; the Python reference exposes enable_sleeptime. Sources, agent creation
SupermemoryRequest related memories using the documented include option. SourceInspect related memories and version relationships around each hit. SourceObserve document.status and document.dreaming_status reaching "done", then repeat retrieval. Source
MnemoverseDefault expansion returns association-found items labelled source: hebbian_retrieval; our live reads observed them. ReferenceInspect query_concepts, expanded_concepts, and each item’s source. ReferenceSubmit feedback(atom_ids, outcome, query_concepts=...) to teach associations used in subsequent sessions. Getting started

The decision becomes more concrete after these checks. Keep the query, the returned items, the provenance, and the later outcome together. That record lets you distinguish a plausible answer from a memory mechanism you can explain.

Common questions

How can I implement long-term associative memory in Python?

Store notes with concepts, record weighted links between concepts, expand reads through those links, and save both notes and links between sessions. The standard-library example on this page demonstrates that loop; a production implementation also needs indexed storage, coordination between concurrent writers, and shared access across tools.

Is vector search the same as associative memory?

No. Similarity search can retrieve related wording without an explicit term match. The associative behaviour tested here retrieves an item through a stored relationship, rather than relying only on its similarity to the query.

Can I run the Python associative memory example without an API key?

Yes. The example uses only json, os, and collections.defaultdict. Save it as assoc_min.py and run python assoc_min.py in an empty directory. It needs no API key or service.

How does Mnemoverse expose associative retrieval?

Mnemoverse read expands through associations by default. Inspect query_concepts, expanded_concepts, and each item's source. The source value hebbian_retrieval identifies an item reached through learned associations.

How do associations improve between agent sessions?

In the example, feedback changes concept-link weights and save preserves them for a later load. In Mnemoverse, feedback(atom_ids, outcome, query_concepts=...) updates valence and Hebbian associations, creating co-activation links between query concepts and result concepts.

Sources

Documentation read 2026-09-20; independent review and stated corrections dated 2026-09-21. Mnemoverse facts and live-read observations verified 2026-09-21.

Mem0

Cognee

Zep

Letta

Supermemory

Mnemoverse

Edward Izgorodin · Mnemoverse · 2026-09-22

Mnemoverse is a persistent-memory API for AI agents. Free key: console.mnemoverse.com · Plans and limits · Docs: Getting Started