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:
| Operation | Purpose |
|---|---|
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 removedOr 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_idjournals under a per-user key, not the bare session id (before 0.17 it was the session id), so build that key withjournal_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
| Backend | profile | forget | export |
|---|---|---|---|
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.
Recommended pattern
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.