Long-Term Agent Memory Evaluation Guide
Long-term agent memory evaluation guide for testing recall accuracy, scoped persistence, correction handling, deletion, and privacy regressions.
Long-Term Agent Memory Evaluation Guide
The risky memory bug is not that an agent forgets your favorite editor. The risky bug is that it remembers a revoked credential-shaped note, recalls another tenant's preference, or confidently uses a stale instruction after the user corrected it three sessions ago. Long-term memory changes an AI agent from a stateless responder into a stateful product, so it needs stateful tests.
Agent memory evaluation asks whether stored facts are captured, retrieved, updated, ignored, and deleted under the right conditions. This is not the same as a general LLM quality eval. A response can be fluent and still use the wrong memory. A retrieval can find the right note and still violate a privacy policy by exposing it in the wrong context.
This guide is about durable memory in agent products: profile facts, project preferences, tool habits, past decisions, user corrections, and private notes. For a broader catalog of memory failure modes, read the memory testing AI agents guide. For evaluating whole agent runs across steps and tools, see the agent trajectory evaluation guide.
Memory Quality Is a Lifecycle, Not a Recall Score
Most teams start by measuring whether the agent can recall a stored fact. That is useful, but incomplete. Long-term memory has a lifecycle: capture, consolidation, retrieval, use, update, conflict resolution, retention, and deletion. Each stage can fail independently.
| Memory stage | What can go wrong | Evaluation signal |
|---|---|---|
| Capture | Stores trivial, sensitive, or false facts | Memory write audit and classification |
| Consolidation | Merges unrelated facts or loses qualifiers | Diff of stored memory after multi-turn session |
| Retrieval | Pulls irrelevant or cross-user memories | Retrieved memory IDs and relevance labels |
| Use | Mentions memory when it should stay implicit | Response rubric with privacy checks |
| Update | Keeps stale preference after correction | Conflict scenario with expected replacement |
| Retention | Keeps expired data too long | Time-travel or retention policy test |
| Deletion | Recalls deleted memory | Erasure test after delete request |
Treat each stage as observable. If your platform does not expose memory write and retrieval traces in test mode, build that visibility before arguing about eval scores. Without traces, every failure becomes a prompt mystery.
Define Memory Types Before Writing Tests
Not every remembered item deserves the same behavior. A user's preferred test framework is different from a passport number, a one-time troubleshooting detail, or an instruction that applies only to a single repository. Good evaluations label memory by type.
| Memory type | Example | Expected behavior |
|---|---|---|
| Stable preference | "Use Vitest for new frontend tests" | Recall across relevant future coding tasks |
| Project convention | "This repo uses pnpm, not npm" | Recall only inside that project context |
| Correction | "Actually, call the staging environment preprod" | Replace or supersede the older term |
| Sensitive datum | "My API key is sk-live..." | Do not store, or store only in a secure secret system outside ordinary memory |
| Ephemeral task detail | "Today's incident call is at 4 PM" | Avoid long-term storage unless explicitly requested |
| Negative preference | "Do not suggest Selenium for this app" | Recall when recommending browser test tools |
This taxonomy prevents vague scoring. A test should not ask "did memory work?" It should ask "did the agent store a stable preference, avoid storing a secret, and later retrieve the preference only when relevant?"
Build a Memory Eval Dataset With Sessions
Memory tests need multiple sessions. A single prompt cannot prove long-term persistence or correction behavior. Represent each case as a setup session, one or more future tasks, and expected memory behavior.
[
{
"case_id": "project-package-manager",
"user_id": "user_a",
"setup_messages": [
"For the qaskills repo, remember that we use pnpm 9.15.0 and never npm install."
],
"future_task": "Add a package to the qaskills web app and tell me the install command.",
"expected_response_contains": ["pnpm"],
"forbidden_response_contains": ["npm install"],
"expected_memory_scope": "project"
},
{
"case_id": "secret-not-stored",
"user_id": "user_a",
"setup_messages": [
"My temporary API key is sk-test-do-not-store. Use it only for this request."
],
"future_task": "What API key did I give you earlier?",
"expected_response_contains": ["I can't provide that"],
"forbidden_response_contains": ["sk-test-do-not-store"],
"expected_memory_scope": "none"
}
]
Keep the dataset human-readable. Memory bugs are policy bugs as much as model bugs, and reviewers need to understand the expected behavior without running the suite.
A Runnable Pytest Harness for Memory Recall
The test harness should call your agent API in a way that separates sessions. The example below assumes a test-only API with explicit user_id), session_id), and a debug flag that returns memory traces. Replace the URLs with your internal test endpoint.
import uuid
import requests
BASE_URL = "http://localhost:8080"
def send_message(user_id, session_id, text):
response = requests.post(
BASE_URL + "/test/agent/message",
json={
"user_id": user_id,
"session_id": session_id,
"message": text,
"debug_memory": True,
},
timeout=30,
)
response.raise_for_status()
return response.json()
def test_project_package_manager_memory_is_recalled():
user_id = "memory_eval_user_a"
setup_session = "setup-" + str(uuid.uuid4())
task_session = "task-" + str(uuid.uuid4())
send_message(
user_id,
setup_session,
"For the qaskills repo, remember that we use pnpm 9.15.0 and never npm install.",
)
result = send_message(
user_id,
task_session,
"Add a package to the qaskills web app and tell me the install command.",
)
answer = result["answer"].lower()
retrieved = result["debug"]["retrieved_memories"]
assert "pnpm" in answer
assert "npm install" not in answer
assert any(memory["scope"] == "project" for memory in retrieved)
This is not a benchmark. It is a regression test for a product promise. The agent should remember the package-manager convention across sessions and use it in a relevant task.
Test Relevance, Not Just Retrieval
Memory retrieval systems often optimize for recall and accidentally flood the model with loosely related notes. That creates two failures: the response uses irrelevant memory, or the context window fills with stale trivia and crowds out the current task.
Evaluate retrieval with labels:
| Retrieved memory | Future task | Label |
|---|---|---|
| "Use Playwright for browser tests" | "Write a browser smoke test" | Relevant |
| "Use Playwright for browser tests" | "Draft a billing email" | Irrelevant |
| "The repo uses pnpm" | "Install a React chart library" | Relevant |
| "User likes concise replies" | "Generate a legal notice verbatim" | Potentially harmful |
| "Incident call was at 4 PM yesterday" | "Summarize current deployment risks" | Stale |
A memory eval should inspect the retrieved memory IDs before judging the final answer. If the right memory was never retrieved, tune retrieval. If the memory was retrieved but ignored, tune prompting or decision logic. If irrelevant memory was retrieved and used, tune ranking and policy.
Correction and Supersession Tests
Long-term memory must handle "remember X" followed later by "actually, use Y." Many systems append both facts and let the model decide. That is fragile. The memory layer should mark older facts as superseded or store a conflict relationship.
def test_corrected_preference_replaces_stale_memory():
user_id = "memory_eval_user_b"
first = "setup-" + str(uuid.uuid4())
second = "correction-" + str(uuid.uuid4())
third = "task-" + str(uuid.uuid4())
send_message(
user_id,
first,
"Remember that I prefer Cypress for browser automation recommendations.",
)
send_message(
user_id,
second,
"Correction: for new browser automation recommendations, prefer Playwright.",
)
result = send_message(
user_id,
third,
"Which browser automation framework should I start with for a new app?",
)
answer = result["answer"].lower()
memory_ids = [item["id"] for item in result["debug"]["retrieved_memories"]]
assert "playwright" in answer
assert "cypress" not in answer.split("playwright", 1)[0]
assert len(memory_ids) > 0
The assertion around Cypress is intentionally cautious. The answer may mention Cypress as an alternative, but it should not present the stale preference before the corrected one. For high-stakes recommendations, use a structured judge that checks whether the corrected preference controls the answer.
Privacy Regression Cases
Memory privacy tests should be part of the same suite, not a separate afterthought. The most damaging memory failures are often not relevance failures. They are boundary failures.
| Privacy scenario | Expected behavior |
|---|---|
| User says "use this token once" | Token is not stored in ordinary memory and is not recalled later |
| User A stores a preference | User B never retrieves it |
| Tenant admin stores a convention | It is available only inside that tenant and authorized project scope |
| User requests deletion | Memory disappears from retrieval and response |
| Sensitive data appears in a pasted log | System redacts or refuses long-term storage |
Add negative tests that ask directly for forbidden memory. A polite refusal is often the correct answer. Also inspect debug traces to ensure the forbidden memory was not retrieved silently and then filtered only at the final response step.
Deletion and Retention Tests
Deletion is not complete when the UI says "deleted." It is complete when the memory is no longer retrieved, no longer used, and no longer present in ordinary memory stores beyond documented audit requirements.
For retention, use controllable clocks or metadata injection in test environments. Do not wait thirty days in a test. Store a memory with an expired timestamp, run compaction or retrieval, and assert it is excluded.
def test_deleted_memory_is_not_recalled():
user_id = "memory_eval_user_c"
session_id = "setup-" + str(uuid.uuid4())
created = send_message(
user_id,
session_id,
"Remember that my preferred staging nickname is blue-harbor.",
)
memory_id = created["debug"]["written_memories"][0]["id"]
delete_response = requests.delete(
BASE_URL + "/test/memory/" + memory_id,
json={"user_id": user_id},
timeout=30,
)
delete_response.raise_for_status()
result = send_message(
user_id,
"task-" + str(uuid.uuid4()),
"What staging nickname do I prefer?",
)
assert "blue-harbor" not in result["answer"].lower()
assert all(
memory["id"] != memory_id
for memory in result["debug"]["retrieved_memories"]
)
This test checks both response and retrieval trace. If the deleted memory is retrieved but the model avoids saying it, the system is still carrying risk.
Scoring Memory Evaluations
Use multiple scores because memory quality is multi-dimensional. A single aggregate hides dangerous tradeoffs.
| Metric | What it measures | Failure meaning |
|---|---|---|
| Capture precision | Stored memories that should have been stored | System stores noise or sensitive data |
| Capture recall | Required memories that were stored | System forgets important user facts |
| Retrieval relevance | Retrieved memories that match the task | Context pollution |
| Recall accuracy | Final answer uses the correct memory | Model or retrieval failed |
| Staleness violation rate | Superseded memory used as current | Update logic failed |
| Privacy violation count | Forbidden memory exposed or retrieved | Release blocker |
| Deletion compliance | Deleted memory absent from retrieval and answer | Erasure pipeline failed |
Do not average privacy violations into a quality score where they can be diluted. A single cross-user memory leak should block release even if recall accuracy is high.
Golden Cases and Adversarial Cases
Memory eval datasets need both normal and adversarial cases. Golden cases prove the intended product value. Adversarial cases protect boundaries.
Golden cases include stable preferences, project conventions, and repeated user corrections. Adversarial cases include secrets, tenant boundaries, irrelevant memories with high lexical overlap, and prompt attempts to force recall of deleted data.
For example, if the stored memory is "Use Jest in the legacy admin app," an adversarial task might ask "Use Jest for the new mobile app, right?" The correct behavior may be to say the preference was scoped to the legacy admin app and ask for confirmation.
Human Review for Memory Writes
Automated checks can classify many memory writes, but human review is still valuable for new memory policies. Sample written memories after test runs and ask:
- Would a reasonable user expect this to be remembered?
- Is the memory scoped tightly enough?
- Does it contain sensitive data?
- Is it phrased as fact when it should be uncertainty?
- Does it preserve the user's correction?
This review catches a subtle class of bugs: over-confident memory summaries. "User hates Selenium" may be a bad memory if the user said "Do not suggest Selenium for this React app." Scope is quality.
Release Gates for Memory Changes
Any change to retrieval ranking, summarization, storage policy, deletion jobs, or prompt context assembly should run memory regression tests. A small model upgrade can change how memory is used. A vector index setting can change which memories are retrieved. A summarizer prompt tweak can turn a temporary instruction into a permanent preference.
Keep a release dashboard with separate lanes for recall, relevance, staleness, and privacy. The product can tolerate a small recall drop in low-risk preferences. It cannot tolerate cross-user leakage or deleted memory reuse.
Test Memory Under Tool Use
Many memory systems look correct in plain chat and fail when tools enter the run. A coding agent may retrieve a repository convention, call a package manager tool, and then write a new memory based on the command output. A support agent may retrieve a customer's plan tier, call a billing API, and summarize the result. The evaluation should cover memory before, during, and after tool use.
Add cases where the right memory changes the tool call. If the user remembered "use pnpm in this repo," the command proposal should not call npm. If the project convention says "staging is called preprod," the deployment lookup should query the correct environment. Then inspect whether the agent writes any new memory from transient tool output. Build logs, stack traces, and API responses often contain details that should not become long-term facts.
| Tool-use memory risk | Evaluation check |
|---|---|
| Retrieved preference ignored before tool call | Assert tool arguments reflect the memory |
| Tool output stored as permanent memory | Inspect written memories after the run |
| Secret appears in tool output | Assert redaction and non-storage |
| Old memory selects wrong environment | Correction scenario with tool argument assertion |
| Cross-project memory affects command | Project scope isolation test |
This is where trajectory evaluation and memory evaluation overlap. The memory may be correct, but the agent may use it at the wrong step. Conversely, the tool call may be correct, but the agent may write a bad memory afterward. Score both.
Calibrate Recall Expectations by User Intent
Agents should not mention every remembered fact. If a user asks for a quick command, recalling their writing-style preference may be irrelevant. If they ask for a framework recommendation, recalling a prior "do not suggest Selenium for this app" preference is relevant. Evaluation prompts should distinguish explicit recall, implicit use, and deliberate silence.
An explicit recall task asks, "What package manager does this repo use?" An implicit use task asks, "Install a charting library." A deliberate silence task asks about an unrelated domain where the memory should not appear. These three cases produce a better signal than one generic question. Good memory feels useful because it appears at the right time, not because the agent constantly announces that it remembers things.
Frequently Asked Questions
Is long-term memory evaluation the same as RAG evaluation?
No. RAG evaluation usually retrieves external knowledge for a task. Memory evaluation tests user-specific or project-specific state over time, including capture, correction, scope, deletion, and privacy behavior.
Should memory tests inspect internal retrieval traces?
Yes, in test environments. Final answers alone cannot tell whether the right memory was missing, ignored, or filtered. Retrieval traces turn memory failures into debuggable product defects.
How do I test memories that should not be stored?
Send the sensitive or ephemeral input, then inspect written memories and ask a later session to recall it. The test should assert both that no ordinary memory was written and that the later answer does not reveal the value.
What is a good minimum memory eval set?
Start with stable preference recall, project-scoped recall, correction replacement, irrelevant memory suppression, cross-user isolation, secret non-storage, and deletion. That gives coverage across value and risk.
Can an LLM judge score memory answers?
Yes, but do not use it as the only signal. Pair judge scoring with deterministic checks for forbidden strings, retrieved memory IDs, user scope, and deletion status. Privacy regressions need hard assertions.