Long-Term Memory Stores

This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

This page was written against langchain 1.4.3, langchain-core 1.6.5, langgraph 1.2.12, langgraph-checkpoint-postgres 3.1.2 and langchain-ollama 1.1.0 on Python 3.12, as verified on 2026-09-29. The running example is DocsAssistant, which answers questions over this site’s documentation using Ollama’s llama3.1:8b chat model and nomic-embed-text embedding model over the pgvector store built in Integrating with LangChain. Ollama was not available while the snippets were checked, so every output shown here was produced with a scripted fake chat model (GenericFakeChatModel) and, where a snippet needs embeddings, a toy keyword-count function in place of nomic-embed-text; the similarity scores shown are therefore illustrative, and no output comes from a live model. The PostgresStore snippets are the exception on the storage side: they ran against a real PostgreSQL 17.11 server with pgvector 0.8.6 started in a throw-away container.

An agent with only a checkpointer remembers a conversation and nothing else: start a new thread and it has forgotten that Alice wants Java examples. Long-term memory is the other half. It stores facts, preferences and past episodes outside any thread, so the next conversation, the next week’s conversation, or a different agent altogether can read them. In LangGraph this is the job of a store, and in LangChain 1.x an agent gets one by passing it to create_agent. This page covers the store API, the two implementations most projects use, how to organise memories per user, how to search them by meaning, how an agent writes them, and how to keep the whole thing private and bounded. The taxonomy of memory types (semantic, episodic, procedural), extraction and compression, and the comparison with LangChain4j and Spring AI live in Memory; the vector-store side of the same idea, with a conversation-search schema and semantic caching, is in Agent Memory & Semantic Cache. Thread-scoped memory is covered in Short-Term Memory & Checkpointers.

Threads versus stores

A checkpointer saves the state of one thread after every step, keyed by thread_id. A store is a separate key-value database that any thread can read and write, organised by namespace and key. The two are independent and are usually used together:

flowchart LR subgraph T1[thread alice-1] S1[checkpointed state] end subgraph T2[thread alice-2] S2[checkpointed state] end subgraph T3[thread bob-1] S3[checkpointed state] end T1 -->|remember| A[("docsassistant / alice / preference")] T2 -->|recall| A T3 -->|recall| B[("docsassistant / bob / preference")] A --- ST[(store)] B --- ST

Two threads that belong to Alice see the same memories because they read the same namespace; Bob’s thread cannot see them because the application derives the namespace from who is asking, not from anything the model says. That is the design rule the rest of the page builds on.

Each memory is a JSON document (a Python dict) with a key, stored under a namespace (a tuple of strings, like a folder path). The store adds created_at and updated_at timestamps. LangChain’s long-term-memory page describes the same model: "LangGraph stores long-term memories as JSON documents in a store", each under a custom namespace and a distinct key. See LangChain — Long-term memory and LangGraph — Stores.

The BaseStore API

Every store implements BaseStore from langgraph.store.base. Its core methods are put, get, delete, search and list_namespaces, each with an a-prefixed async twin (aput, aget, adelete, asearch, alist_namespaces). InMemoryStore keeps everything in a dictionary, which is right for tests and development. This snippet exercises the whole surface:

The BaseStore operations on InMemoryStore
from langgraph.store.memory import InMemoryStore

store = InMemoryStore()
alice = ("docsassistant", "alice", "preference")

store.put(alice, "language", {"text": "Answer with Java examples only", "source": "chat"})
store.put(("docsassistant", "alice", "episode"), "e1", {"text": "Asked how to tune hnsw ef_search"})
store.put(("docsassistant", "bob", "preference"), "language", {"text": "Answer with Python examples"})

item = store.get(alice, "language")                 # one item by exact namespace + key
print(item.value, item.key, item.namespace)

print([i.key for i in store.search(("docsassistant", "alice"))])          # prefix match
print(store.list_namespaces(prefix=("docsassistant",), max_depth=2))
print(store.search(("docsassistant", "alice"), filter={"source": "chat"})[0].value)

store.delete(alice, "language")
print(store.get(alice, "language"))
print(store.search(("docsassistant", "bob"))[0].dict())
Output
{'text': 'Answer with Java examples only', 'source': 'chat'} language ('docsassistant', 'alice', 'preference')
['language', 'e1']
[('docsassistant', 'alice'), ('docsassistant', 'bob')]
{'text': 'Answer with Java examples only', 'source': 'chat'}
None
{'namespace': ['docsassistant', 'bob', 'preference'], 'key': 'language', 'value': {'text': 'Answer with Python examples'}, 'created_at': '2026-09-29T13:17:04.136128+00:00', 'updated_at': '2026-09-29T13:17:04.136128+00:00', 'score': None}

Things to notice:

  • put with an existing namespace and key overwrites that item, which is how a memory is updated in place.

  • get returns an Item (or None); search returns SearchItem objects, which add a score that is None unless a semantic query was given.

  • search takes a namespace prefix, not an exact namespace. ("docsassistant", "alice") returns items under both preference and episode; pass the full namespace to restrict to one level. Its filter argument matches on fields of the stored value (\{"source": "chat"} above), and limit (default 10) and offset page through results. The official page warns that results past limit are silently truncated, so set it above the expected maximum or page with offset.

  • list_namespaces discovers what exists (prefix, suffix, max_depth), for example to iterate over users.

  • Result ordering is backend-dependent when no query is given: the LangGraph docs note that the Postgres stores order by updated_at descending, while InMemoryStore returns insertion order. Do not rely on either.

Namespaces per user

The namespace is the store’s only built-in access boundary, so its design is a security decision, not just tidiness. A workable layout for DocsAssistant is ("docsassistant", <user_id>, <kind>): an application prefix (so several apps can share one database), the user, and a memory kind such as preference or episode. Prefix matching then gives both views for free: search ("docsassistant", "alice") for everything about Alice, or add the kind to see only her preferences.

The user ID must come from authenticated application code, never from a tool argument the model fills in. In create_agent the clean way is the run context: define a small dataclass, pass it as context_schema, supply the value at invoke time, and read it in tools through ToolRuntime. The model can neither see nor change it. The memory tools below all follow this pattern; the next section shows the failure it prevents.

The kind of memory also affects the shape of the values. The LangChain memory overview distinguishes a single, continuously updated profile document (one key per user, overwritten on update) from a collection of many small documents (one key per memory, searched at read time). Profiles are cheap to read but error-prone to rewrite as they grow; collections give better recall but need search and clean-up. DocsAssistant uses a collection, because "prefers Java" and "asked about ef_search on Tuesday" are separate facts that age at different rates. See LangChain — Memory overview.

Semantic search in stores

By default search filters and pages; it does not understand meaning. To search by similarity, give the store an index when you construct it. The index config has three parts: an embed function or embeddings object, the vector size dims, and the fields of the stored value to embed (a JSON-path style list; "$" means the whole document). Any object that turns strings into vectors works, including a plain function. This snippet uses a toy keyword-count function so it runs without a model server:

Semantic search over memories with a toy embedding function
import math

from langgraph.store.memory import InMemoryStore

VOCAB = ["java", "python", "index", "hnsw", "search", "prefer", "spring", "postgres"]


def embed(texts: list[str]) -> list[list[float]]:
    """Toy bag-of-keywords embedding so the demo runs without Ollama."""
    vectors = []
    for text in texts:
        counts = [float(text.lower().count(word)) for word in VOCAB]
        norm = math.sqrt(sum(c * c for c in counts)) or 1.0
        vectors.append([c / norm for c in counts])
    return vectors


store = InMemoryStore(index={"embed": embed, "dims": len(VOCAB), "fields": ["text"]})
ns = ("docsassistant", "alice", "preference")
store.put(ns, "a", {"text": "I prefer Java and Spring examples"})
store.put(ns, "b", {"text": "I use python notebooks"})
store.put(ns, "c", {"text": "Tune the hnsw index in postgres search"})
store.put(ns, "d", {"text": "internal java note"}, index=False)   # stored, never embedded

for hit in store.search(ns, query="how do I tune the hnsw index?", limit=3):
    print(hit.key, hit.score, hit.value["text"])
print([i.key for i in store.search(ns, filter={"text": "internal java note"})])
Output
c 0.7071067811865476 Tune the hnsw index in postgres search
a 0.0 I prefer Java and Spring examples
b 0.0 I use python notebooks
['d']

Three details matter. The query argument turns search into a similarity search and fills in each result’s score; filter can still be combined with it. A per-item index argument on put overrides fields: a list of field names embeds only those, and index=False stores the item without embedding it, so it stays retrievable by key or filter (d above) but never appears in semantic results. And the vector size must match the model exactly.

DocsAssistant would use the same nomic-embed-text model as its document index (768 dimensions), through langchain-ollama. LangChain’s init_embeddings accepts a provider:model string, and the store takes the resulting object in embed:

Configuring the index with DocsAssistant’s real embedding model
from langchain.embeddings import init_embeddings
from langgraph.store.memory import InMemoryStore

embeddings = init_embeddings("ollama:nomic-embed-text")
print(type(embeddings).__name__, embeddings.model)

store = InMemoryStore(index={"embed": embeddings, "dims": 768, "fields": ["text"]})
Output
OllamaEmbeddings nomic-embed-text

Constructing the objects needs no server; the first put or search would call Ollama, which was not available here. Use the same embedding model for indexing and querying, and pick memory text that reads like a statement ("Prefers Java examples") rather than a fragment, because the embedded text is what gets compared with the query. Semantic memory in the psychological sense (facts about a user) is a different idea from semantic search (retrieval by meaning); a store gives you both. See LangGraph — Stores and LangSmith — Semantic search.

PostgresStore for production

InMemoryStore loses everything when the process exits and cannot be shared between workers. For DocsAssistant, whose vectors already live in PostgreSQL with pgvector, PostgresStore is the natural durable choice. It is provided by the langgraph-checkpoint-postgres package, which the official page installs together with the recommended Psycopg extra:

Installing the Postgres store
pip install -U langgraph-checkpoint-postgres "psycopg[binary]"

PostgresStore.from_conn_string opens a connection and is used as a context manager. The first run needs store.setup(), which creates the tables. The connection string is read from an environment variable rather than written in code. The same API and the same semantic index config as InMemoryStore then apply; this snippet ran against a real PostgreSQL 17.11 / pgvector 0.8.6 server:

The same operations on PostgresStore
import math
import os

from langgraph.store.postgres import PostgresStore

DB_URI = os.environ["DOCSASSISTANT_DB_URI"]      # e.g. a postgresql:// URL; never hard-code credentials

VOCAB = ["java", "python", "index", "hnsw", "search", "prefer", "spring", "postgres"]


def embed(texts):
    vectors = []
    for text in texts:
        counts = [float(text.lower().count(word)) for word in VOCAB]
        norm = math.sqrt(sum(c * c for c in counts)) or 1.0
        vectors.append([c / norm for c in counts])
    return vectors


with PostgresStore.from_conn_string(
    DB_URI, index={"embed": embed, "dims": len(VOCAB), "fields": ["text"]}
) as store:
    store.setup()                                  # creates the tables; safe to call again
    ns = ("docsassistant", "alice", "preference")
    store.put(ns, "a", {"text": "I prefer Java and Spring examples"})
    store.put(ns, "b", {"text": "I use python notebooks"})
    store.put(ns, "c", {"text": "Tune the hnsw index in postgres search"})
    print(store.get(ns, "a").value)
    for hit in store.search(("docsassistant", "alice"), query="tune hnsw index", limit=2):
        print(hit.key, round(hit.score, 3), hit.value["text"])
    print(store.list_namespaces(prefix=("docsassistant",)))
    store.delete(ns, "b")
    print([i.key for i in store.search(ns)])
Output
{'text': 'I prefer Java and Spring examples'}
c 0.707 Tune the hnsw index in postgres search
a 0.0 I prefer Java and Spring examples
[('docsassistant', 'alice', 'preference')]
['c', 'a']

After setup() the database contained the tables store and store_vectors (plus two migration bookkeeping tables); the embeddings live in store_vectors using pgvector. Results without a query came back most recently updated first (c, then a), matching the ordering the docs describe. Some practical points:

  • AsyncPostgresStore (same package, langgraph.store.postgres) is the async variant for async graphs and servers; from_conn_string also accepts pipeline and pool_config options for pipelining and connection pooling when a single connection is not enough.

  • Point the store at the same PostgreSQL instance as the rest of DocsAssistant when you want one thing to back up and one thing to secure, but keep memories in their own tables: the store manages its own schema and migrations, which is unrelated to the PGVector tables from Integrating with LangChain.

  • Redis, MongoDB and other backends exist as integrations; the official page links the list. If you write your own, subclass BaseStore and implement the async methods; when the backend has no vector search, raise NotImplementedError for a query.

  • When the agent runs on the LangSmith Agent Server (see Deployment), the platform provides the store for you, and you configure it in langgraph.json, as shown in the retention section below.

Giving an agent a store

To let an agent use the store, pass it to create_agent(…​, store=store). Tools then reach it through ToolRuntime, exactly as the state and context injection on Tools works: a parameter typed ToolRuntime[YourContext] is hidden from the model’s schema, and runtime.store and runtime.context are filled in at call time. The other half of the wiring is a checkpointer for the thread’s own messages (see Agents with create_agent).

Memory-writing tools

Long-term memory only helps if something writes it. The most direct pattern is the hot path: give the agent a tool to save a fact and a tool to look facts up, and let the model decide when to call them. The tools take the namespace from the run context, so the model supplies only the content:

DocsAssistant’s remember and recall tools
from dataclasses import dataclass
from datetime import datetime, timezone

from langchain.agents import create_agent
from langchain.tools import ToolRuntime, tool
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.store.memory import InMemoryStore


@dataclass
class UserContext:
    user_id: str                       # set by the application after authentication


store = InMemoryStore(index={"embed": embed, "dims": len(VOCAB), "fields": ["text"]})


@tool
def remember(fact: str, kind: str, runtime: ToolRuntime[UserContext]) -> str:
    """Save one durable fact about this user. kind is "preference" or "episode"."""
    namespace = ("docsassistant", runtime.context.user_id, kind)
    key = datetime.now(timezone.utc).isoformat()
    runtime.store.put(namespace, key, {"text": fact})
    return f"saved as {kind}"


@tool
def recall(query: str, runtime: ToolRuntime[UserContext]) -> list[str]:
    """Search what you remember about this user."""
    hits = runtime.store.search(("docsassistant", runtime.context.user_id), query=query, limit=3)
    return [h.value["text"] for h in hits]


agent = create_agent(
    model,                              # a scripted fake model in the run below
    tools=[remember, recall],
    context_schema=UserContext,
    checkpointer=InMemorySaver(),       # short-term: one thread's messages
    store=store,                        # long-term: shared across threads
)


def ask(text, thread, user):
    out = agent.invoke({"messages": [("user", text)]}, {"configurable": {"thread_id": thread}},
                       context=UserContext(user_id=user))
    return out["messages"]


m = ask("I only use Spring AI, Java answers please.", "alice-1", "alice")
print(len(m), m[-1].content)
m = ask("What language do I prefer?", "alice-2", "alice")          # new thread, same user
print([x.content for x in m if x.type == "tool"][0])
print(m[-1].content)
m = ask("What language do I prefer?", "bob-1", "bob")              # different user
print([x.content for x in m if x.type == "tool"][0], "|", m[-1].content)
print(store.list_namespaces())
Output
4 Noted, Java only from now on.
["Prefers Java only, spring examples"]
You prefer Java and Spring examples.
[] | I do not know your preferences yet.
[('docsassistant', 'alice', 'preference')]

The scripted model (a GenericFakeChatModel subclass whose bind_tools returns itself, fed AIMessage objects with tool_calls) called remember on the first turn and recall on the next two; the point is what the store did. Thread alice-2 starts with an empty message history yet recall found the preference, because the store is shared across threads and the namespace is keyed by user. Bob’s thread ran the same recall and got an empty list, because the user ID came from the context the application supplied. Had remember taken a user_id argument, a prompt-injected model could have written to, or read from, any namespace it chose to name. This is the same least-privilege reasoning that Tool Design applies to every stateful tool.

A tool that writes memory is a write action, and it deserves the controls other write actions get. Keep the payload small and typed (one fact and a kind, not a free-form dump), return a short confirmation, and think about whether some saves need human approval before they persist; the approval mechanism is covered in Human-in-the-Loop & Time Travel and the design rules in Human-in-the-Loop.

Reading and writing from graph nodes

Not every memory should depend on the model deciding to call a tool. Inside a LangGraph node, the same store and the run context arrive through a Runtime parameter, which LangGraph injects when the node declares it. The official memory overview names the two write timings: on the hot path (the agent saves as it goes, with real-time updates but extra latency and extra work for the model) and in the background (a separate step or job extracts memories from the conversation, at the cost of deciding when to run it). The graph below reads relevant memories before the model call and then runs a write step after it. The extraction step is a stand-in (a keyword test); in practice it would be an LLM call with structured output (see Structured Output) that returns zero or more distilled facts:

A graph that reads memories before the model and writes them afterwards
import asyncio
import uuid
from dataclasses import dataclass

from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
from langchain_core.messages import AIMessage
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, MessagesState, StateGraph
from langgraph.runtime import Runtime
from langgraph.store.memory import InMemoryStore


@dataclass
class Context:
    user_id: str


model = GenericFakeChatModel(messages=iter([AIMessage("Here is a Java answer."), AIMessage("Java again.")]))


async def call_model(state: MessagesState, runtime: Runtime[Context]):
    namespace = (runtime.context.user_id, "memories")
    hits = await runtime.store.asearch(namespace, query=state["messages"][-1].content, limit=3)
    known = "\n".join(h.value["text"] for h in hits)
    system = f"Known about the user:\n{known}" if known else "Nothing known about the user."
    print("system prompt ->", system.replace("\n", " | "))
    reply = await model.ainvoke([("system", system), *state["messages"]])
    return {"messages": [reply]}


async def write_memory(state: MessagesState, runtime: Runtime[Context]):
    last_user = [m for m in state["messages"] if m.type == "human"][-1].content
    if "prefer" in last_user.lower():                # stand-in for an LLM extraction step
        await runtime.store.aput((runtime.context.user_id, "memories"), str(uuid.uuid4()), {"text": last_user})


builder = StateGraph(MessagesState, context_schema=Context)
builder.add_node(call_model)
builder.add_node(write_memory)
builder.add_edge(START, "call_model")
builder.add_edge("call_model", "write_memory")
builder.add_edge("write_memory", END)

store = InMemoryStore(index={"embed": embed, "dims": len(VOCAB), "fields": ["text"]})
graph = builder.compile(checkpointer=InMemorySaver(), store=store)


async def main():
    cfg1, cfg2 = {"configurable": {"thread_id": "1"}}, {"configurable": {"thread_id": "2"}}
    await graph.ainvoke({"messages": [("user", "I prefer Java only, with Spring")]}, cfg1, context=Context("alice"))
    await graph.ainvoke({"messages": [("user", "explain the hnsw index in Java")]}, cfg2, context=Context("alice"))


asyncio.run(main())
Output
system prompt -> Nothing known about the user.
system prompt -> Known about the user: | I prefer Java only, with Spring

The second thread’s model call received the memory written during the first thread. The graph is compiled with both a checkpointer and a store, which is the pattern the official Stores page shows; the checkpointer keeps each thread separate while the store carries what should survive. Treat text read from the store as untrusted input when it goes into a prompt, for the reasons in the next section. For the graph API itself see LangGraph Fundamentals. See LangGraph — Stores.

Privacy and retention

A memory store is a database of personal data about your users, and it needs the same care as any other. The generic obligations are set out on Memory and in Agent Guardrails & Safety; this section shows how they map onto the LangGraph store.

  • Minimise. Store the distilled fact ("prefers Java examples"), not the transcript it came from, and never store secrets, credentials or special-category data. Validate what a memory tool accepts, and consider PII redaction before writes (see Guardrails & Security).

  • Isolate by user. Build the namespace from authenticated context, as above. The store has no per-namespace permission model of its own, so isolation is entirely the application’s responsibility.

  • Treat memories as untrusted input. A memory written from a prompt-injected conversation can steer every later one. Keep writes in an audit trail and prefer narrow, structured memories to arbitrary text that is replayed into the system prompt.

  • Expire what should not live forever. The store supports a time-to-live (TTL) per item.

  • Make deletion real. A "forget me" request must reach the store as well as the checkpointer, chat history and any other index.

Retention is configured through a TTL. default_ttl is a number of minutes, and items get it unless a put overrides it with its own ttl (also in minutes; ttl=None means "never expires"). With omit_expired set, an expired item stops appearing in reads immediately, before a sweep physically deletes it, and refresh_on_read controls whether reading an item resets its timer. The snippet below ran against the same PostgreSQL server with a three-second default so the effect is visible:

Per-item TTL on PostgresStore
import os
import time

from langgraph.store.postgres import PostgresStore

DB_URI = os.environ["DOCSASSISTANT_DB_URI"]
ttl = {"default_ttl": 0.05, "refresh_on_read": False, "omit_expired": True}   # 0.05 min = 3 s

with PostgresStore.from_conn_string(DB_URI, ttl=ttl) as store:
    store.setup()
    ns = ("docsassistant", "carol", "episode")
    store.put(ns, "e1", {"text": "asked about ef_search"})               # inherits the 3 s default
    store.put(ns, "e2", {"text": "kept"}, ttl=None)                      # never expires
    print("before:", sorted(i.key for i in store.search(ns)))
    time.sleep(4)
    print("after :", sorted(i.key for i in store.search(ns)))            # expired item is omitted
    print("swept :", store.sweep_ttl())                                  # and later physically deleted
Output
before: ['e1', 'e2']
after : ['e2']
swept : 1

sweep_ttl() deletes expired rows on demand, and the store can also run a periodic sweeper (TTLConfig has a sweep_interval_minutes key; PostgresStore has start_ttl_sweeper / stop_ttl_sweeper). These names were checked against the installed package; the official pages document the same settings for deployments. When the agent runs on LangSmith’s Agent Server, you set them in langgraph.json under store.ttl, next to the embedding index under store.index. The configured default_ttl applies only to items created after the change, so older items must be deleted manually:

Store index and TTL in langgraph.json
{
  "dependencies": ["."],
  "graphs": { "docsassistant": "./docsassistant/agent.py:agent" },
  "store": {
    "index": { "embed": "ollama:nomic-embed-text", "dims": 768, "fields": ["text"] },
    "ttl": { "refresh_on_read": true, "sweep_interval_minutes": 120, "default_ttl": 43200 }
  }
}

The JSON parses; it was not deployed here. Each deployment supports a single embedding model. Expiry handles the passage of time. A user’s request to be forgotten is different: it has to remove everything under the user’s namespace prefix, however old. Because the store has no "delete a namespace" call, delete item by item, paging until the search comes back empty:

Forgetting a user across all their memories
from langgraph.store.base import BaseStore


def forget_user(store: BaseStore, user_id: str, page_size: int = 100) -> int:
    """Delete every item under ("docsassistant", user_id, ...) and return how many were removed."""
    removed = 0
    while items := store.search(("docsassistant", user_id), limit=page_size):
        for item in items:
            store.delete(item.namespace, item.key)
            removed += 1
    return removed


store.put(("docsassistant", "alice", "preference"), "language", {"text": "Java only"})
store.put(("docsassistant", "alice", "episode"), "e1", {"text": "asked about ef_search"})
store.put(("docsassistant", "bob", "preference"), "language", {"text": "Python"})
print("removed:", forget_user(store, "alice"))
print("alice:", store.search(("docsassistant", "alice")))
print("bob:", [i.key for i in store.search(("docsassistant", "bob"))])
Output (identical on InMemoryStore and PostgresStore)
removed: 2
alice: []
bob: ['language']

Because every memory of Alice’s sits under one prefix, deletion is a single loop; a namespace layout that scattered her data across unrelated prefixes would have made it a search problem. Also delete her threads from the checkpointer (see Short-Term Memory & Checkpointers) and any other stores that hold copies. Broader guidance on data protection for AI systems belongs to the planned AI Security sub-section. See LangSmith — Configure TTL.

Where to go next

The table summarises the pieces and where each was shown:

Table 1. Long-term memory building blocks
Need LangChain / LangGraph tool Shown in

Development store

InMemoryStore

BaseStore operations

Durable, shared store

PostgresStore, AsyncPostgresStore

PostgresStore for production

Per-user isolation

Namespace from run context

Namespaces per user

Find memories by meaning

index={"embed", "dims", "fields"}, search(query=…​)

Semantic search in stores

Save and look up memories

remember / recall tools with ToolRuntime; Runtime in nodes

Memory-writing tools

Expire and forget

TTLConfig, sweep_ttl, forget_user

Privacy and retention

Memory that the agent reads is context the agent is given, so what you retrieve competes with everything else in the prompt; that trade-off is discussed in Context Engineering for Agents and applied to middleware in Middleware. To inspect what an agent read and wrote, see Observability with LangSmith.