# Spec: Agent-Session-Speaker Model — Part 1 (Core + Models + Pipelines)

> Generated via /vibeflow:gen-spec on 2026-03-26
> Budget: ≤ 25 files (cross-cutting rename, budget override justified)

## Objective

Renomear `user_id` → `agent_id` em todo o código fonte + adicionar `session_id` ao MemoryEvent, alinhando o SDK com o modelo mental correto: memória é do agente, contexto é da sessão, identidade é do speaker.

## Context

O SDK usa `user_id` como identificador genérico — às vezes significa "a pessoa", às vezes "o agente", às vezes "a sessão". Essa ambiguidade causa confusão conceitual e impede features como multi-speaker e sessões compartilhadas.

A mudança é breaking (v0.8.0) mas não tem usuários externos. O Inspector (time de testes interno) será notificado.

## Definition of Done

1. **[ ] Zero ocorrências de `user_id` em `src/arandu/`** — grep retorna vazio. Todas renomeadas pra `agent_id`.
2. **[ ] `session_id` existe no MemoryEvent** — campo `session_id: str` com default `"default"` no modelo e na API pública (`write()` e `retrieve()`).
3. **[ ] API pública usa `agent_id` + `session_id` + `speaker_name`** — `MemoryClient.write(agent_id, message, speaker_name, session_id="default")` e `MemoryClient.retrieve(agent_id, query, session_id=None)`.
4. **[ ] Todos os testes passam** — 370+ testes, CI limpo (ruff + mypy + pytest).
5. **[ ] `pyproject.toml` version = `"0.8.0"`** — bump pra major breaking change.
6. **[ ] Convenções seguidas** — naming snake_case, fail-safe pattern, sem `any` types novos.

## Scope

### 1. `src/arandu/models.py`
- Renomear `user_id` → `agent_id` em TODOS os modelos (MemoryFact, MemoryEntity, MemoryEntityAlias, MemoryEntityRelationship, MemoryEvent, MemoryCluster, MemoryMetaObservation, MemoryAttributeRegistry, MemoryFactEntityLink, MemoryIntention, SessionObservation)
- Adicionar `session_id = Column(String, nullable=False, default="default")` ao MemoryEvent

### 2. `src/arandu/client.py`
- `MemoryClient.__init__()`: manter interface atual
- `write()`: renomear `user_id` → `agent_id`, adicionar `session_id: str = "default"`
- `retrieve()`: renomear `user_id` → `agent_id`, adicionar `session_id: str | None = None` (None = busca cross-session)
- `WriteResult` e `RetrieveResult`: sem mudança de campos

### 3. `src/arandu/write/*.py` (6 files)
- pipeline.py, extract.py, entity_resolution.py, reconcile.py, upsert.py, canonicalization.py, correction.py, entity_helpers.py, pending_ops.py
- Renomear `user_id` → `agent_id` em todas as funções e queries

### 4. `src/arandu/read/*.py` (7 files)
- pipeline.py, retrieval.py, retrieval_agent.py, query_expansion.py, graph_retrieval.py, spreading_activation.py, emotional_trends.py, procedural.py
- Renomear `user_id` → `agent_id` em todas as funções e queries

### 5. `src/arandu/background/*.py` (4 files)
- clustering.py, consolidation.py, memify.py, sleep_time.py
- Renomear `user_id` → `agent_id`

### 6. `tests/*.py` (8 files)
- Renomear `user_id` → `agent_id` em todos os testes
- Adicionar teste de `session_id` no write

### 7. `pyproject.toml`
- Version bump: `0.7.2` → `0.8.0`

## Anti-scope

- **Docs** — Part 2 cuida da documentação. Este spec é só código.
- **Session-based retrieval** — `session_id` no retrieve é filtro opcional, não muda lógica de scoring
- **Session context injection** — Não injetar entidades da sessão no prompt de extração (scope futuro)
- **Migration script** — Não criar script de migração de banco (sem usuários)
- **Renomear tabelas** — Só colunas. Nomes de tabelas ficam como estão.

## Technical Decisions

### TD-1: `agent_id` em vez de `agent_name`
O campo é um identificador técnico (string livre), não um nome humano. `agent_id` é consistente com o pattern de `session_id` e `speaker_name`.

### TD-2: `session_id` default = `"default"`
Em vez de gerar UUID automático (o que criaria sessão nova a cada call), o default `"default"` mantém backward compatibility: quem não passa session_id tem uma sessão implícita. Alinha com o caso WhatsApp (sessão infinita).

### TD-3: `session_id` armazenado apenas no MemoryEvent
Facts, entities e relationships NÃO têm session_id. São do agente, não da sessão. O session_id está no evento (o registro bruto) pra rastreabilidade de contexto.

### TD-4: Rename mecânico com replace_all
A maioria das mudanças é `s/user_id/agent_id/g`. Usar replace_all do editor pra consistência. Verificar cada arquivo manualmente pra edge cases (strings em prompts, logs, etc).

## Applicable Patterns

- **Data Access** — Queries async via `select().where()`. Sem mudança de pattern, só rename de coluna.
- **Error Handling** — Fail-safe mantido. Rename não afeta try/except.
- **Testing** — Mock-based com AsyncMock. Rename nos mocks.

## Risks

1. **Rename parcial** — Se esquecer um `user_id` em algum lugar obscuro, query vai falhar em runtime. Mitigação: grep final + CI.
2. **Column rename no ORM** — SQLAlchemy usa o nome Python do atributo, não o nome da coluna. Se o modelo diz `agent_id = Column("user_id", ...)`, a coluna no banco continua `user_id`. Mitigação: renomear o `Column()` name também, já que não tem banco existente pra migrar.
3. **Prompts com "user_id"** — Alguns prompts de LLM podem mencionar `user_id`. Mitigação: grep em strings de prompt.

## Dependencies

Nenhuma — este é Part 1.
