# Long-term Memory

> How to build agents that learn and remember across conversations using semantic memory storage and retrieval.

Source: https://10xgraph.com/docs/concepts/memory-and-store
Last updated: 2026-10-08

Your agent needs to remember facts across conversations: a user's preferences, resolved questions, learned skills. The **memory store** makes this possible by providing a semantic, searchable database of memories that persists independently of the thread. It works alongside the checkpointer to create a two-level recall system: fast thread-specific state and cross-thread semantic knowledge.

## Why long-term memory matters

As an agent serves multiple users and conversations, it accumulates context that is too specific or expensive to carry in every message:

- A user mentioned their timezone once; future conversations should know it.
- You discovered an error-handling workaround; later conversations should learn from it.
- You extracted a fact from a document; it should be available without re-uploading.

The checkpointer solves this within a thread (conversation continuity), but it cannot share knowledge across threads. The memory store adds that cross-thread layer, enabling agents to learn from experience and personalize behavior over time.

## How it works: memory store vs checkpointer

The **checkpointer** saves your full agent state (messages, context, custom fields) to resume a thread. The **memory store** saves individual semantic memories you want to recall across threads. They solve different problems:

| Aspect | Checkpointer | Memory store |
| --- | --- | --- |
| Scope | One thread | Cross-thread, cross-user |
| Content | Full `AgentState` snapshot | Individual semantic records |
| Lifetime | Until thread is deleted | Until explicitly deleted |
| Access | By `thread_id` | By semantic similarity |
| Use case | Conversation continuity | Facts, preferences, knowledge |

For example, if a user says "I'm in UTC-5", the checkpointer remembers it for that thread. The memory store lets you *save* it (to find later) and *retrieve* it in any future thread when the agent asks "What timezone is the user in?"

## How the agent uses memory

The LLM doesn't passively receive memories (except in preload mode); it **calls a tool** to work with them. There are two ways to get these tools, and they differ:

| Setup | Tools the model sees |
|---|---|
| `Agent(memory=MemoryConfig(...))` in `postload` mode | `user_memory_tool` (`action="search"` or `"remember"`, with `text`) and, if agent memory is enabled, the read-only `agent_memory_tool` (`query`) |
| `MemoryIntegration(...).tools` | A single `memory_tool` with `action="store"`, `"search"`, `"update"` or `"delete"` |

With `user_memory_tool` the model never supplies memory ids; the memory layer manages identity and deduplication.

The `memory_tool` flow looks like this:

```
LLM thinks: "I should remember this timezone"
       ↓
Calls: memory_tool(action="store", content="User is in UTC-5", memory_key="user_timezone")
       ↓
Result: {"status": "scheduled", "action": "store"}
       ↓
Future thread: LLM calls memory_tool(action="search", query="user timezone")
       ↓
Result: JSON list of matches, retrieved by semantic similarity, not keyword lookup
```

**Deduplication (`memory_tool`):** if you call `store` with the same `memory_key`, the old record is updated rather than duplicated. When no key is given, a similarity check (score of 0.95 or higher) skips near-identical content.

**Non-blocking writes:** memory writes run in the background via `BackgroundTaskManager`, so they never delay the agent's response. A search first waits for pending writes to finish.

## Retrieval modes: when do memories reach the LLM?

The framework supports three strategies (`no_retrieval`, `preload`, `postload`) for when the LLM sees memories. `MemoryConfig` defaults to `postload`; `MemoryIntegration` defaults to `no_retrieval`. Choose based on how much memory context you want injected, and whether you prefer the agent to drive retrieval:

### No retrieval (default)

The LLM **cannot read** past memories. With `MemoryIntegration`, it can still write them through `memory_tool`. With `Agent(memory=MemoryConfig(retrieval_mode="no_retrieval"))`, no memory tools are exposed.

**When to use:** Your agent is mostly stateless, or memories are only for logging/analytics.

| Pros | Cons |
|---|---|
| Fast; no store queries | No cross-conversation learning |
| Simple; minimal overhead | Agent can't retrieve past context |

### Preload

Relevant memories are **retrieved automatically** and injected as a system message **before** the LLM runs.

**How it works:** 
1. The user's latest message is used as the search query (override it with `query_builder` on `MemoryIntegration`).
2. Memories matching that query (by semantic similarity) are fetched.
3. They are injected as a system message that starts with `[Long-term Memory Context]`.
4. The LLM then sees the conversation with these memories in context.

With `Agent(memory=...)` this happens inside the Agent before each model call. With `MemoryIntegration`, `memory.wire(...)` adds a separate preload node.

**When to use:** Your agent should proactively use past knowledge. Ideal for retrieval-augmented generation (RAG), customer support, or personalization where the agent should *always* have relevant context available.

| Pros | Cons |
|---|---|
| Agent sees memories by default | Extra latency (preload query) |
| Good for RAG patterns | More tokens injected per message |
| Transparent to agent | Noisy if memories are not well-curated |

### Postload

The LLM **decides when to retrieve** memories by calling the memory tool with `action="search"` itself.

**How it works:** The memory tool is offered alongside your other tools. When the agent wants to check past context, it calls the tool with a query, receives results, and decides what to do with them.

**When to use:** Your agent should *selectively* retrieve memories only when needed. Ideal for conversational agents that can ask "Do I know about this user?" or agents that are memory-efficient and don't need context injected automatically.

| Pros | Cons |
|---|---|
| Agent controls memory use | More tool calls (latency) |
| Avoids injecting irrelevant context | Requires the agent to know to retrieve |
| Efficient | Agent might forget to retrieve |

### Trade-off table

| Aspect | No retrieval | Preload | Postload |
|---|---|---|---|
| Memory injection | Never | Automatic (every message) | On-demand (agent calls tool) |
| Latency | Lowest | Higher (one search per message) | Variable (depends on agent) |
| Best for | Stateless agents | RAG, personalization | Selective retrieval |
| Token cost | Lowest | Highest (if many memories match) | Variable |

## Memory scoping: user vs agent memory

You can partition memories into **scopes** so different agents or users see different memories:

| Scope | Who can access | Use case |
|---|---|---|
| **User memory** (`UserMemoryConfig`, enabled by default) | The LLM can search and write | Facts about a specific user (timezone, preferences, history) |
| **Agent memory** (`AgentMemoryConfig`, disabled by default) | The LLM can only search | Shared knowledge scoped by `agent_id` or `app_id` (domain facts, workarounds) |

**Example:** A customer support agent remembers the customer's billing cycle (user memory). Your own code seeds agent memory with known workarounds, and the model reads them through `agent_memory_tool`. The model has no tool to write agent memory.

Both scopes can share one backing store or each take their own `store`, and they are queried separately.

## Choosing: memory tools (postload) vs preload

The most important decision is **how the agent accesses memory**. This determines your latency, token usage, and agent behavior.

**Use postload (memory tools) if:**
- The agent should decide when to recall, not every message.
- You want to minimize token overhead and latency.
- Your agent is sophisticated enough to know when to ask "Do I have context on this?"
- Example: A conversational assistant that occasionally checks past notes, but doesn't need proactive context.

**Use preload if:**
- Memories should always be available without the agent asking.
- You're building RAG and expect relevant memories to match most queries.
- You want the simplest behavior: the agent always sees relevant context.
- Token cost is secondary to ensuring the agent has context.
- Example: A customer support bot that needs a user's history for every message.

**Use no_retrieval if:**
- You don't need cross-conversation memory at all, only in-thread state.
- Memories are for analytics or audit, not agent behavior.
- You want the fastest, simplest setup.

---

## Available store backends

The two main options are **Qdrant** (vector database you host) and **Mem0** (managed service):

| Store | Hosting | Embeddings | Good for |
|---|---|---|---|
| `QdrantStore` | Local or cloud Qdrant | Your choice (OpenAI, Google, custom) | Full control, self-hosted |
| `Mem0Store` | Mem0 managed service | Mem0-provided | Low ops, Mem0 handles scaling |

Both use semantic similarity search, so searches match meaning, not keywords. A query like "What's my timezone?" will find a memory "I'm UTC-5" even though the words are different.

### Setting up Qdrant locally

```bash
pip install "10xgraph[qdrant,openai]"
```

```python
from tenxgraph.storage.store import (
    create_local_qdrant_store,
    OpenAIEmbedding,
)

store = create_local_qdrant_store(
    collection="agent-memories",
    path="./memory_data",
    embedding=OpenAIEmbedding(),  # requires OPENAI_API_KEY
)
```

This creates a persistent database on disk. For production, use `create_cloud_qdrant_store(url=..., api_key=..., embedding=...)` for Qdrant Cloud, or `create_remote_qdrant_store(host=..., port=..., embedding=...)` for a server you run. `GoogleEmbedding` is also available. For Mem0, install the `mem0` extra and use `create_mem0_store`.

## Implementation patterns

### Pattern 1: With the high-level `Agent` class

This is the simplest approach. Pass `memory` to `Agent` and the framework registers the memory tools and prompt. You still build the graph and pass the store to `compile`:

```bash
pip install "10xgraph[qdrant,openai,google-genai]"
```

```python
from tenxgraph.core.graph import Agent, StateGraph, ToolNode
from tenxgraph.core.state import AgentState, Message
from tenxgraph.storage.store import (
    MemoryConfig,
    OpenAIEmbedding,
    create_local_qdrant_store,
)
from tenxgraph.utils import END, tool

store = create_local_qdrant_store(
    collection="user-memories",
    path="./memory_data",
    embedding=OpenAIEmbedding(),  # requires OPENAI_API_KEY
)

@tool
def search_web(query: str) -> str:
    """Search the web for information."""
    return f"No results for {query}"

tool_node = ToolNode([search_web])

agent = Agent(
    model="google/gemini-2.5-flash",
    tool_node=tool_node,
    memory=MemoryConfig(
        store=store,
        retrieval_mode="postload",  # the model calls user_memory_tool when needed
        limit=5,                    # max memories per search
    ),
)

def route(state: AgentState) -> str:
    last = state.context[-1] if state.context else None
    if last and last.role == "assistant" and last.tools_calls:
        return "TOOL"
    return END

graph = StateGraph()
graph.add_node("MAIN", agent)
graph.add_node("TOOL", tool_node)
graph.add_conditional_edges("MAIN", route, {"TOOL": "TOOL", END: END})
graph.add_edge("TOOL", "MAIN")
graph.set_entry_point("MAIN")
app = graph.compile(store=store)

result = app.invoke(
    {"messages": [Message.text_message("What's my timezone?")]},
    config={"thread_id": "t1", "user_id": "user-42"},
)
```

The `Agent` class automatically:
1. Adds memory instructions to the system prompt (unless `inject_system_prompt=False`).
2. Registers `user_memory_tool` (and `agent_memory_tool` if enabled) on the `ToolNode`, in `postload` mode only.
3. Scopes memories by `user_id` from the config.

In `postload` mode you **must** give the Agent a `ToolNode` (or the name of a `ToolNode` graph node); otherwise the framework raises `RuntimeError`.

### Pattern 2: With `StateGraph` and `MemoryIntegration`

For more control (custom routing, multiple nodes), use `MemoryIntegration` with `StateGraph`:

```python
from tenxgraph.core.graph import Agent, StateGraph, ToolNode
from tenxgraph.core.state import AgentState
from tenxgraph.storage.store import (
    MemoryIntegration,
    OpenAIEmbedding,
    create_local_qdrant_store,
)
from tenxgraph.utils import END, tool

store = create_local_qdrant_store(
    collection="agent-memories",
    path="./memory_data",
    embedding=OpenAIEmbedding(),
)

memory = MemoryIntegration(
    store=store,
    retrieval_mode="preload",  # inject memories before the LLM runs
    limit=5,
)

@tool
def search_web(query: str) -> str:
    """Search the web for information."""
    return f"No results for {query}"

# memory.tools is [memory_tool], so the model can still write memories
tool_node = ToolNode([search_web, *memory.tools])

agent = Agent(
    model="google/gemini-2.5-flash",
    system_prompt=[{"role": "system", "content": memory.system_prompt}],
    tool_node="TOOLS",
)

def route(state: AgentState) -> str:
    last = state.context[-1] if state.context else None
    if last and last.role == "assistant" and last.tools_calls:
        return "TOOLS"
    return END

graph = StateGraph()
graph.add_node("AGENT", agent)
graph.add_node("TOOLS", tool_node)
graph.add_edge("TOOLS", "AGENT")
graph.add_conditional_edges("AGENT", route, {"TOOLS": "TOOLS", END: END})

# Adds the preload node and sets the entry point
memory.wire(graph, entry_to="AGENT")

app = graph.compile(store=store)
```

Invoke it as in Pattern 1. Key differences:
- You build the graph yourself, so you have full control over nodes and edges.
- `memory.tools` gives you the single `memory_tool` to add alongside your own tools.
- `memory.wire(graph, entry_to="AGENT")` sets the entry point and, in preload mode, inserts the preload node.
- `memory.system_prompt` is a string with the LLM instructions for the configured mode.

### MemoryConfig options

For `Agent(memory=...)`, customize memory behavior:

```python
from tenxgraph.storage.store import (
    AgentMemoryConfig,
    MemoryConfig,
    UserMemoryConfig,
)

config = MemoryConfig(
    store=store,                       # The vector store
    retrieval_mode="postload",         # "no_retrieval" | "preload" | "postload" (default "postload")
    limit=5,                           # Max memories per search (default 5)
    score_threshold=0.0,               # Min similarity (default 0.0)
    max_tokens=None,                   # Optional: limit tokens in injected memories
    inject_system_prompt=True,         # Auto-add memory instructions
    user_memory=UserMemoryConfig(
        enabled=True,                  # User-scoped memories the LLM can search and write
        memory_type="episodic",
        category="general",
        limit=5,
    ),
    agent_memory=AgentMemoryConfig(
        enabled=True,                  # Read-only for the LLM (default False)
        memory_type="semantic",
        agent_id="my-agent",
    ),
)
```

## How memories are written and retrieved

With `MemoryIntegration`, the LLM **writes** memories by calling `memory_tool`:

```python
memory_tool(
    action="store",
    content="User is in UTC-5 timezone",
    memory_key="user_timezone",          # Key for deduplication
    memory_type="episodic",
    category="preferences",
)
# Returns {"status": "scheduled", "action": "store"}. If "user_timezone"
# already existed, it is updated.
```

It **searches** (in postload mode) by calling:

```python
memory_tool(action="search", query="What timezone is the user in?")
# Returns JSON: a list of matches with content and score
```

With `Agent(memory=...)` the calls are `user_memory_tool(action="remember", text="...")` and `user_memory_tool(action="search", text="...")`.

**In preload mode**, the framework does the search automatically and injects the results as a system message starting with `[Long-term Memory Context]`.

## Accessing memory in the REST API

When a store is configured in `10xgraph.json`, the server exposes memory endpoints:

```bash
POST   /v1/store/memories              # Store a new memory
POST   /v1/store/memories/list         # List memories for a user
POST   /v1/store/search                # Search memories by query
POST   /v1/store/memories/{id}         # Get a memory
PUT    /v1/store/memories/{id}         # Update a memory
DELETE /v1/store/memories/{id}         # Delete a memory
POST   /v1/store/memories/forget       # Forget memories matching filters
```

See [REST API: Memory store](/docs/reference/rest-api/memory-store) for request/response schemas.

Memories are scoped by the authenticated user's `user_id` (`anonymous` when there is none), so users do not see each other's memories.

## Configuring memory in 10xgraph.json

Point the API server to a memory store:

```json
{
  "agent": "graph:app",
  "store": "graph.dependencies:my_memory_store"
}
```

The `store` key expects a `module:attribute` path pointing to a `BaseStore` instance. The server uses it for the `/v1/store` endpoints.

---

## Next steps

- **To set up memory:** See the task guide [Use memory store](/docs/guides/use-memory-store).
- **For REST API details:** See [REST API: Memory store](/docs/reference/rest-api/memory-store).
- **To learn about thread memory:** See [Checkpointing and threads](/docs/concepts/checkpointing-and-threads) (in-thread state is different from long-term memory).
- **To understand scoping:** See [Authorization scopes](/docs/guides/authorization-scopes) (user isolation is handled by auth).
