> For the complete documentation index, see [llms.txt](https://docs.telys.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.telys.ai/recipes/agent-long-term-memory.md).

# Agent long-term memory

Give an agent durable memory that survives process restarts: one Telys collection, partitioned per user, that you write observations into and recall from before each turn. The store lives on disk under a path you control — no external database, no network calls.

The pattern repeats across agent frameworks:

1. **One collection**, partitioned by a `scope` key built with `scope_key(...)`.
2. **Write** observations with `add_texts` (append-only); correct facts with `upsert_texts`.
3. **Recall** the most relevant memories with `search_text`, scoped to the user with `Eq`.
4. **Persist** with `save()` so memory survives restarts.

{% hint style="info" %}
Telys embeds and searches through the signed native runtime. Install it with `pipx install telys` (or `pip install telys`), sign in with `telys login`, then verify with `telys runtime verify`. The `Algenta*` embedders also need the native kernel, which the verified install provides. See [Install Telys](/start-here/install.md) and [Verify your install](/start-here/verify-install.md).
{% endhint %}

## Why partition per user

Every memory belongs to one user, and a query must never leak another user's memories. Telys enforces this at the storage layer: the collection is partitioned by a `scope` column, and a scoped query only touches its own partition. Compose the scope key with `scope_key(...)`, which joins its parts with a unit separator so keys never collide:

```python
from telys import scope_key

scope_key("user", "alice")                       # -> "user\x1falice"
scope_key("user", "alice", "project", "telys")   # multi-part, still one key
```

{% hint style="warning" %}
`partition_by` takes a **single** column name. Pass a list or tuple and Telys uses only the first element — it is **not** composite. To partition on more than one attribute, compose one string with `scope_key(...)` and store it under a single column (here, `"scope"`). See [Partitions & scope keys](/understand-telys/partitions-scope-key.md).
{% endhint %}

## Full example

`AgentMemory` wraps a Telys collection with the four operations an agent needs: remember an observation, recall relevant memories, correct a fact, and forget by id.

{% code title="agent\_memory.py" lineNumbers="true" %}

```python
from telys import Telys, Eq, scope_key
from telys.embedding import AlgentaMultigramEmbedder


class AgentMemory:
    """Durable, per-user memory backed by a single Telys collection."""

    def __init__(self, path: str = "./agent-memory", name: str = "memories"):
        self.telys = Telys(path)
        self.embedder = AlgentaMultigramEmbedder()  # lexical, on-device
        try:
            self.col = self.telys.open_collection(name, embedder=self.embedder)
        except FileNotFoundError:
            self.col = self.telys.create_collection(
                name,
                dim=self.embedder.profile.dimension,   # dim comes from the kernel
                partition_by="scope",                  # one physical key column
                embedder=self.embedder,
                filter_columns=("kind", "text"),       # round-trip via with_metadata
            )
            self.col.save()

    def _scope(self, user_id: str) -> str:
        return scope_key("user", user_id)

    def remember(self, user_id: str, mem_id: str, text: str, kind: str = "observation") -> None:
        """Append a new observation. Raises KeyError if mem_id already exists."""
        scope = self._scope(user_id)
        self.col.add_texts(
            texts=[text],
            ids=[f"{scope}:{mem_id}"],
            metadata=[{"scope": scope, "kind": kind, "text": text}],
        )

    def remember_fact(self, user_id: str, mem_id: str, text: str) -> None:
        """Write or overwrite a durable fact (idempotent)."""
        scope = self._scope(user_id)
        self.col.upsert_texts(
            texts=[text],
            ids=[f"{scope}:{mem_id}"],
            metadata=[{"scope": scope, "kind": "fact", "text": text}],
        )

    def recall(self, user_id: str, query: str, top_k: int = 5) -> list[dict]:
        """Return the most relevant memories for this user only."""
        res = self.col.search_text(
            query,
            top_k=top_k,
            where=Eq("scope", self._scope(user_id)),
            with_metadata=True,
        )
        return [
            {"id": i, "score": s, "text": m["text"], "kind": m["kind"]}
            for i, s, m in zip(res["ids"], res["scores"], res["metadata"])
        ]

    def forget(self, user_id: str, mem_id: str) -> None:
        scope = self._scope(user_id)
        self.col.delete([f"{scope}:{mem_id}"])

    def save(self) -> None:
        self.col.save()


if __name__ == "__main__":
    mem = AgentMemory()

    # A durable fact + a couple of observations for one user.
    mem.remember_fact("alice", "pref-lang", "Alice prefers answers in Python.")
    mem.remember("alice", "obs-1", "Alice is building a CLI for invoice parsing.")
    mem.remember("alice", "obs-2", "Alice hit a bug with timezone-naive datetimes.")

    # Another user's memory stays isolated.
    mem.remember_fact("bob", "pref-lang", "Bob prefers answers in Go.")

    for hit in mem.recall("alice", "what language should I answer in?"):
        print(f"{hit['score']:.3f}  [{hit['kind']}]  {hit['text']}")

    mem.save()
```

{% endcode %}

## Recall before every turn

Recall first, generate second. Before building the prompt, pull the most relevant memories for the current user and inject them as context. After the turn, write any new observations back.

```python
def handle_turn(mem: AgentMemory, user_id: str, user_message: str) -> str:
    # 1. Recall — scoped to this user, ranked by relevance to the message.
    recalled = mem.recall(user_id, user_message, top_k=5)
    context = "\n".join(f"- {h['text']}" for h in recalled)

    # 2. Build your prompt with `context` and call your model of choice.
    #    (See "Use with OpenAI & Anthropic" for the retrieval-augmented call.)
    prompt = f"What you know about the user:\n{context}\n\nUser: {user_message}"
    reply = generate_reply(prompt)  # your LLM call

    # 3. Remember anything durable from this turn.
    mem.remember(user_id, mem_id=f"turn-{abs(hash(user_message)) & 0xffff:x}",
                 text=f"User asked: {user_message}")
    mem.save()
    return reply
```

## Expected result

Running `agent_memory.py` prints Alice's memories ranked by relevance, the language preference highest. Bob's memory never appears — the query is scoped to Alice:

```
0.812  [fact]  Alice prefers answers in Python.
0.144  [observation]  Alice is building a CLI for invoice parsing.
0.061  [observation]  Alice hit a bug with timezone-naive datetimes.
```

After `save()`, the collection is written under `./agent-memory/memories/`; `collection.json` is the commit point. Re-running the script reopens the saved collection with the memories intact — that is the long-term part.

{% hint style="info" %}
`add_texts` is append-only and raises `KeyError` if an id exists — what you want for observations. Use `upsert_texts` (as `remember_fact` does) to overwrite in place, such as a changing preference. See [Add & upsert records](/working-with-data/add-and-upsert.md).
{% endhint %}

## Next steps

<table data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>Chat memory</strong></td><td>Store conversation turns and prune old history as sessions grow.</td><td><a href="/pages/kFgHki3hX9gk8Key8GcJ">/pages/kFgHki3hX9gk8Key8GcJ</a></td></tr><tr><td><strong>Quickstart: memory for an AI agent</strong></td><td>The end-to-end starter this recipe builds on.</td><td><a href="/pages/PAT4pDE2TcuVbgRbANvN">/pages/PAT4pDE2TcuVbgRbANvN</a></td></tr><tr><td><strong>Partitions &#x26; scope keys</strong></td><td>How <code>scope_key</code> isolates per-user data.</td><td><a href="/pages/Rv5cBr1GHyWvxVC7AeIF">/pages/Rv5cBr1GHyWvxVC7AeIF</a></td></tr></tbody></table>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.telys.ai/recipes/agent-long-term-memory.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
