Memory and Store

In shortHow long-term memory works in 10xGraph — memory_tool, retrieval modes, MemoryIntegration, and MemoryConfig.

  • 5 min read
  • 12 sections
  • Updated
  • v0.9.2
  • Markdown

The memory store provides long-term, cross-thread memory. Unlike the checkpointer (which saves per-thread state), the memory store lets an agent remember facts about users and itself across different conversations.

Checkpointer vs store

Item Checkpointer Memory store
Scope One thread Cross-thread, cross-user
Content Full AgentState snapshot Individual memory records
Lifetime Until thread is deleted Until explicitly deleted
Retrieval Exact key — thread_id Semantic similarity search
Primary use Conversation continuity User preferences, facts, knowledge

How the agent accesses memory: memory_tool

Memory is not injected passively — the LLM calls a tool to interact with it.
memory_tool is an @tool-decorated async function exposed to the agent’s ToolNode.

plaintext
LLM decides to remember/recall
       ↓
  calls memory_tool(action="store"|"search"|"delete", ...)
       ↓
  memory_tool writes / searches BaseStore (Qdrant, Mem0, …)
       ↑
  returns result to LLM

The three supported actions:

Action When to use
action="store" Save a new fact or update an existing one (by memory_key)
action="search" Recall relevant memories matching a query
action="delete" Remove an outdated memory by memory_id

Deduplication is automatic: if you call store with a memory_key that already exists (e.g. "user_name"), the existing record is updated rather than duplicated. As a fallback, near-identical text (similarity ≥ 0.95) is also deduplicated.

Writes are asynchronous — they run in the background via BackgroundTaskManager and never block the LLM’s response.


Retrieval modes

Control when memories flow into the LLM context:

Mode Behaviour
"no_retrieval" (default) LLM cannot read past memories but CAN write new ones via memory_tool
"preload" Relevant memories are retrieved and injected as a system message before the LLM call
"postload" LLM retrieves memories on-demand by calling memory_tool(action="search", ...)

Available store backends

Class Module Backend
QdrantStore tenxgraph.storage.store Qdrant vector database (local or cloud)
Mem0Store tenxgraph.storage.store Mem0 managed memory service

Both backends support semantic similarity search via embeddings.

Creating a local Qdrant store

Python
from tenxgraph.storage.store import QdrantStore, create_local_qdrant_store, OpenAIEmbedding

# Convenience factory (persistent on disk)
store = create_local_qdrant_store(
    collection="agent-memories",
    path="./memory_data",
    embedding=OpenAIEmbedding(),
)

# Or directly
store = QdrantStore(
    embedding=OpenAIEmbedding(),
    path="./memory_data",
    collection="agent-memories",
)

Option A: Using MemoryConfig with Agent

This is the recommended approach when you build an agent with the high-level Agent class.

Python
from tenxgraph.core.graph import Agent, ToolNode
from tenxgraph.storage.store import (
    QdrantStore, MemoryConfig, OpenAIEmbedding,
    create_local_qdrant_store,
)

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

# Tool for the agent's regular work
@tool
def search_web(query: str) -> str:
    ...

tool_node = ToolNode([search_web])

agent = Agent(
    model="gemini/gemini-2.5-flash",
    tools=tool_node,
    memory=MemoryConfig(
        store=store,
        retrieval_mode="postload",   # LLM calls memory_tool to search
    ),
)

Agent.__init__ calls _setup_memory() internally, which:

  1. Appends a system prompt fragment (instructions about how to use memory) to the agent.
  2. Registers memory_tool (and scope-specific tools) onto the existing ToolNode.

You must pass a ToolNode to Agent when memory tools are enabled; the framework will raise a RuntimeError otherwise.

MemoryConfig fields

Python
from tenxgraph.storage.store import MemoryConfig, UserMemoryConfig, AgentMemoryConfig

MemoryConfig(
    store=store,                    # default store used if scope stores are not set
    retrieval_mode="postload",      # "no_retrieval" | "preload" | "postload"
    limit=5,                        # max memories to retrieve
    score_threshold=0.0,            # min similarity score (0.0 = all results)
    max_tokens=None,                # optional token budget for retrieved context
    inject_system_prompt=True,      # auto-add memory instructions to system prompt
    user_memory=UserMemoryConfig(   # user-scoped memory the LLM can search & write
        enabled=True,
        store=store,                # can override the top-level store
        user_id="user-42",          # if None, injected at runtime from config
        memory_type="episodic",
        category="general",
        limit=5,
    ),
    agent_memory=AgentMemoryConfig( # agent/app-scoped memory the LLM can only search
        enabled=False,
        store=store,
        agent_id="my-agent",
        memory_type="semantic",
    ),
)

Option B: Using MemoryIntegration with StateGraph

For lower-level graph control, use MemoryIntegration directly.

Python
from tenxgraph.core.graph import StateGraph, Agent, ToolNode
from tenxgraph.storage.store import (
    MemoryIntegration,
    QdrantStore, OpenAIEmbedding,
    create_local_qdrant_store,
)
from tenxgraph.utils import END

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

memory = MemoryIntegration(store=store, retrieval_mode="preload")

tool_node = ToolNode([search_web, *memory.tools])  # include memory_tool
agent = Agent(model="gemini/gemini-2.5-flash", system_prompt=memory.system_prompt)

graph = StateGraph()
graph.add_node("AGENT", agent)
graph.add_node("TOOLS", tool_node)
graph.add_edge("TOOLS", "AGENT")
graph.add_conditional_edges("AGENT", lambda s: "TOOLS" if s.tool_calls else END)

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

app = graph.compile(store=store)

MemoryIntegration properties

Property Type Description
memory.tools list[Callable] Contains memory_tool — add to your ToolNode
memory.system_prompt str System prompt fragment — pass to your LLM node
memory.preload_node Callable | None Async node function; only set in preload mode
memory.retrieval_mode ReadMode The configured mode
memory.store BaseStore The underlying store instance

wire() method

Python
memory.wire(
    graph,
    entry_to="AGENT",           # the node to run after memory retrieval
    preload_node_name="memory_preload",  # name for the auto-added preload node
)
  • preload mode: adds a memory_preload node, sets it as the graph entry point, edges it to entry_to.
  • no_retrieval / postload: just calls graph.set_entry_point(entry_to).

System prompt fragments

get_memory_system_prompt(mode) returns the correct instructions for the LLM depending on the retrieval mode:

Python
from tenxgraph.storage.store import get_memory_system_prompt

print(get_memory_system_prompt("no_retrieval"))
# → "You do NOT have access to read or search long-term memories. ..."
#   + write instructions (memory_tool store/update/delete)

print(get_memory_system_prompt("preload"))
# → "You have been provided with long-term memory context ..."
#   + write instructions

print(get_memory_system_prompt("postload"))
# → "You have access to a memory_tool that can search, store, and delete ..."
#   (full read + write instructions)

All modes include write instructions — the LLM can always decide to persist new information.


Writing memories: important rules

The system prompt instructs the LLM to:

  • Use action="store" with a short memory_key (e.g. "user_name", "favorite_language").
  • The framework handles deduplication — if the same memory_key exists it updates the old record.
  • Use action="delete" only with an explicit memory_id returned from a prior search.
  • Never use action="update" unless you have a specific memory_id.
Python
# The LLM internally calls something like:
memory_tool(
    action="store",
    content="User's name is Shudipto",
    memory_key="user_name",
    memory_type="semantic",
)

Preload node

In preload mode the _preload_node function:

  1. Extracts the latest user message as the search query.
  2. Searches the store for the top-limit memories by similarity (cross-thread — thread_id is stripped).
  3. Flushes any in-flight background writes first to avoid stale reads.
  4. Returns a [Message.text_message(..., role="system")] list injected into state before the LLM sees the conversation.

You can customise the query extractor:

Python
from tenxgraph.storage.store import create_memory_preload_node

def my_query_builder(state):
    return state.context[-1].text() if state.context else ""

preload = create_memory_preload_node(
    store=store,
    query_builder=my_query_builder,
    limit=5,
    score_threshold=0.3,
)
graph.add_node("memory_preload", preload)

REST API for the store

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

Terminal
POST   /v1/store/memories        # store a memory
POST   /v1/store/search          # search memories by query
GET    /v1/store/memories        # list memories
PUT    /v1/store/memories/{id}   # update a memory
DELETE /v1/store/memories/{id}   # delete a memory

See REST API: Memory store for schemas.

Configuring via 10xgraph.json

JSON
{
  "agent": "graph.react:app",
  "store": "graph.dependencies:my_store"
}

Last updated for v0.9.2Edit this page on GitHubReport an issue