# PRD: CRUD de Fatos no MemoryClient

> Generated via /vibeflow:discover on 2026-04-08

## Problem

O MemoryClient expõe apenas `write()` e `retrieve()`. Qualquer operação de gerenciamento — buscar um fato pelo ID, listar todos os fatos de um agente, deletar um fato — não existe no SDK. Consumidores são forçados a fazer queries SQL diretas nos modelos SQLAlchemy, quebrando a abstração que o SDK deveria prover.

O caso concreto: o MCP server do Arandu implementa `arandu_forget` e `arandu_entities` com SQL direto (`UPDATE memory_facts SET valid_to = now ...`, `SELECT` nos modelos). Isso é uma gambiarra que contorna o SDK porque o SDK não oferece essas operações.

Todo SDK de memória para agentes no mercado (Mem0, Zep, Letta) expõe CRUD completo. O Arandu está atrás nesse básico.

## Target Audience

Consumidores do SDK — qualquer aplicação que integre com o Arandu, incluindo:
- MCP servers (caso de uso primário hoje)
- APIs REST que wrappem o SDK
- Scripts de manutenção e debug

## Proposed Solution

Adicionar 4 métodos públicos ao `MemoryClient`:

1. **`get(agent_id, fact_id)`** — Retorna um fato específico pelo ID, ou `None` se não encontrado.
2. **`get_all(agent_id, *, limit, offset)`** — Lista fatos ativos de um agente com paginação simples (limit + offset).
3. **`delete(agent_id, fact_id)`** — Hard delete de um fato específico (remove do banco).
4. **`delete_all(agent_id)`** — Hard delete de todos os fatos de um agente.

Hard delete é alinhado com o mercado (Mem0, Zep, LangMem, Letta — todos fazem hard delete). O soft delete via `valid_to`/`invalidated_at` continua existindo para o reconcile automático do pipeline, mas a operação explícita do usuário é definitiva.

## Success Criteria

1. MCP server consegue substituir SQL direto por chamadas ao SDK (`client.delete()` em vez de `UPDATE memory_facts SET valid_to...`).
2. Cada método funciona isolado, sem depender do pipeline de write/read.
3. `get_all` suporta paginação (limit + offset) para agentes com muitos fatos.
4. `delete` e `delete_all` fazem hard delete (row removida do banco, não soft delete).
5. Operações são async e seguem o padrão de session management existente (session por operação, commit no final).

## Scope v0

- `get(agent_id, fact_id) -> FactDetail | None`
- `get_all(agent_id, *, limit=100, offset=0) -> list[FactDetail]`
- `delete(agent_id, fact_id) -> bool`
- `delete_all(agent_id) -> int` (retorna quantidade deletada)
- Dataclass `FactDetail` com campos do fato (id, entity_name, entity_key, fact_text, category, confidence, importance, valid_from, created_at, etc.)
- Testes unitários para todos os métodos
- Docs EN + PT-BR
- Export dos novos tipos no `__init__.py`

## Anti-scope

- **`update(id, data)`** — Update manual é bypass do pipeline de reconcile. Updates acontecem via `write()`. Não entra.
- **`history(agent_id, fact_id)`** — Só 2/4 concorrentes têm, e o `supersedes_fact_id` já permite seguir a cadeia. Pode ser adicionado depois sem breaking change.
- **Filtros avançados** — Sem filtro por entity_key, category, data, etc. no v0. `get_all` lista tudo do agente.
- **Cursor-based pagination** — Offset é suficiente pro uso via MCP. Cursor pode vir depois se necessário.
- **Bulk operations** — Sem batch delete por filtro. `delete_all` é tudo-ou-nada.
- **Cascade delete** — `delete` remove o fato e os entity_links (via CASCADE no FK). Não remove entidades, relações, ou eventos associados.

## Technical Context

### O que já existe

- **`MemoryFact` model** (`src/arandu/models.py`): tem `valid_to`, `invalidated_at` para soft delete, `supersedes_fact_id` para versioning. Entity links têm `CASCADE` no FK (`ondelete="CASCADE"` no `MemoryFactEntityLink`).
- **`ScoredFact` dataclass** (`src/arandu/client.py`): já retorna `fact_id` no retrieve. Mas é orientado a retrieval (tem `score`, `scores`). O CRUD precisa de um tipo diferente (`FactDetail`) com campos do modelo.
- **Session management**: `MemoryClient` já tem `_session_factory` (async_sessionmaker). Novos métodos seguem o mesmo padrão: `async with self._session_factory() as session:`.
- **Transaction boundary**: Pipeline functions não fazem commit — o caller (MemoryClient) faz. Mesmo padrão aqui.

### Patterns a seguir

- **Data access** (`.vibeflow/patterns/data-access.md`): async SQLAlchemy, `select().where()`, `session.commit()` no caller.
- **Error handling** (`.vibeflow/patterns/error-handling.md`): savepoints para isolamento, fail-safe defaults.
- **Conventions** (`.vibeflow/conventions.md`): Google-style docstrings, type hints strict (mypy), `snake_case` para métodos.

## Open Questions

None.
