Skip to Content
MemoryGDPR ops

GDPR ops

When a user asks what your agent knows about them, or asks you to delete it, you need to find and erase their data across everything the agent remembered. Every memory backend implements three operations for that:

OperationPurpose
profile(user_id=)Inspect what the agent knows about a user.
forget(user_id=, ...)Right-to-be-forgotten. Full or scoped erasure.
export(user_id=)Data-portability dump (DSAR).

These are built-in methods on the Memory protocol, not optional add-ons, so you don’t have to write backend-specific deletion code.

profile(user_id=)

profile = await agent.memory.profile(user_id="alice") # MemoryProfile( # user_id="alice", # episode_count=12, # fact_count=5, # last_seen=datetime(...), # recent_sessions=["conv_42", "conv_43"], # sample_facts=[ # Fact(subject="alice", predicate="works_at", object="Acme"), # ... # ], # )

Use this in:

  • Customer support tickets (“here’s what we know about you”).
  • Internal admin tools.
  • Compliance reviews.

forget(user_id=, ...)

Full erasure of one user’s data:

deleted = await agent.memory.forget(user_id="alice") # number of rows removed

Or scoped to one conversation, or to data older than a date:

# Just one session await agent.memory.forget(user_id="alice", session_id="conv_42") # Just data before a cutoff from datetime import datetime, UTC await agent.memory.forget( user_id="alice", before=datetime(2025, 1, 1, tzinfo=UTC), )

What gets deleted:

  • Episodes (chat transcripts), including any stored tool transcripts.
  • Facts in the bi-temporal store.
  • The user’s working memory blocks (with before=, those last updated before the cutoff). Before 0.17, forget() left blocks in place.

A session-scoped forget() deletes that session’s episodes only and leaves facts and working blocks alone, because neither belongs to a session.

With auto_extract on, facts are extracted in the background after each run. forget() first stops the extractions of the episodes it erases, so one still running can’t write the erased facts back after it returns. (Before 0.17.3, agent.memory.forget() didn’t. On 0.17.2, call await agent.aclose() before it.)

What does NOT get deleted automatically:

  • The audit log (immutable; legal hold). Filter or redact at query time if you need to.
  • The runtime journal (used for replay). Prune the user’s sessions, or drop the journal database if compliance requires it. A run with a user_id journals under a per-user key, not the bare session id (before 0.17 it was the session id), so build that key with journal_scope:
from loomflow.runtime import journal_scope await agent.runtime.store.prune(session_id=journal_scope("conv_42", "alice"))

On disk. SQLite memory, fact and journal databases set PRAGMA secure_delete, so deleted rows are overwritten in the file instead of staying readable in free pages. On the other backends (Postgres, Redis, Chroma), how soon deleted data leaves the disk and the backups depends on that database.

export(user_id=)

Returns a structured MemoryExport you can serialize to JSON for a Data Subject Access Request:

export = await agent.memory.export(user_id="alice") # MemoryExport( # user_id="alice", # episodes=[Episode(...), ...], # facts=[Fact(...), ...], # blocks=[MemoryBlock(...), ...], # exported_at=datetime(...), # ) payload = export.model_dump_json(indent=2)

MemoryExport is a Pydantic model, so .model_dump_json() gives you a stable serialization with ISO-8601 dates. It covers episodes, facts and the user’s working blocks (blocks, added in 0.17).

Auditing the request

profile(), forget() and export() don’t write audit entries themselves. To get an audit trail, call them from a tool (see below): with audit_log= wired, every tool call is recorded as signed tool_call / tool_result entries carrying the run’s user_id. The HMAC signature covers user_id, so a tampered entry fails verification.

Backend coverage

Backendprofileforgetexport
InMemoryMemory✅✅✅
VectorMemory✅✅✅
SqliteMemory✅✅✅
ChromaMemory✅✅✅
PostgresMemory✅✅✅
RedisMemory✅✅✅

All six backends share the same surface; the implementation differs underneath but the contract is identical.

Forget is a hard delete. There’s no “soft delete” mode. Once you call forget(), the data is gone from memory. Test in a non-prod environment first, and back up the underlying database if you might need recovery.

Wrap the operation in a tool so it’s auditable per call and gated by your permissions policy:

from loomflow import Agent, tool, get_run_context @tool(destructive=True) async def gdpr_forget(target_user_id: str) -> str: """Erase all data for the given user. Requires admin approval.""" agent_ctx = get_run_context() if not agent_ctx.metadata.get("is_admin"): return "ERROR: requires admin role" await agent.memory.forget(user_id=target_user_id) return f"forgot {target_user_id}"

With permissions=StandardPermissions(mode=Mode.DEFAULT) and a Slack approval handler, every forget call requires an explicit human approval and lands in the audit log with user_id attribution.

Last updated on