# PRD: Speaker-Aware Extraction

> Generated via /vibeflow:discover on 2026-03-25
> Atualizado após discovery completo (pesquisa de 30+ sistemas, 80+ empresas, análise de TAM)

## Problem

O Arandu confunde **quem fala** (speaker) com **quem lembra** (agent). O `user_id` é o agente — o cérebro dono da memória. O speaker é a pessoa falando COM esse cérebro.

Hoje, quando o speaker diz "meu nome é Pedro, moro em Botafogo, trabalho na Stone":
1. O prompt força entidade `user` ("The user themselves is always included: name='user'")
2. A LLM cria `user` + `Pedro` como entidades separadas e duplica fatos entre elas
3. `_SELF_REFERENCES` mapeia "eu/me/mim" → `user:self`, mas "Pedro" vira `person:pedro` separado
4. Mirror facts criam fatos espúrios em Botafogo/Stone sobre Pedro

Resultado: ~15 fatos para ~5 esperados. Memória vira lixo.

Este bug bloqueia o caso de uso #1 do Arandu (companion AI, onde memory intensity é 5/5) e todos os demais.

## Target Audience

Devs integrando o Arandu SDK para construir agentes com memória. Foco primário: companion/social AI (1 speaker ↔ 1 agent). Foco secundário: customer support (N speakers ↔ 1 agent).

## Proposed Solution

Adicionar `speaker_name` como parâmetro obrigatório no `write()`. O sistema passa a saber quem tá falando e extrai fatos do ponto de vista do agente ouvindo o speaker.

- Prompts reescritos: "The speaker is {speaker_name}. First-person pronouns refer to the speaker."
- Entidade "user" nunca mais é forçada
- Pronomes resolvem pro speaker, não pro `user:self`
- Mirror facts eliminados

## Success Criteria

Mensagem: "Meu nome é Pedro, moro no Rio de Janeiro, no bairro de Botafogo. Eu tenho 34 anos, trabalho na Stone como Senior Group Product Manager."

Com `speaker_name="Pedro"`:
- **3 entidades**: Pedro (person), Botafogo/Rio de Janeiro (place), Stone/StoneCo (organization)
- **~5 fatos**, todos com subject "Pedro": nome, idade, cidade/bairro, empresa, cargo
- **Relações**: Pedro → lives_in → Rio de Janeiro, Pedro → works_at → StoneCo, etc.
- **Zero** entidade "user"
- **Zero** fatos duplicados entre entidades
- **Zero** mirror facts (Stone não tem fato "Stone emprega Pedro")

## Scope v0

- [ ] `client.py` — Adicionar `speaker_name: str` como parâmetro obrigatório no `write()`. Passar pro pipeline.
- [ ] `extract.py` — Reescrever `ENTITY_SCAN_PROMPT`: remover "user is always included", adicionar "The speaker is {speaker_name}". Pronomes → speaker. Proibir pronomes como entidades.
- [ ] `extract.py` — Reescrever `FACT_EXTRACTION_PROMPT`: remover "Use 'user' as subject", adicionar "Use '{speaker_name}' as subject for facts about the speaker". Proibir mirror facts explicitamente.
- [ ] `extract.py` — Ajustar `RELATION_EXTRACTION_PROMPT`: usar speaker_name em vez de "user".
- [ ] `extract.py` — Propagar `speaker_name` pelas funções de extração (`_entity_scan`, `_extract_facts_for_batch`, `_multi_pass_extract`, `extract_from_message`).
- [ ] `entity_resolution.py` — `_SELF_REFERENCES` resolve pra `person:{speaker_name_normalized}` em vez de `user:self`. Renomear pra `_SPEAKER_REFERENCES`.
- [ ] `entity_resolution.py` — Propagar `speaker_name` em `_resolve_single()` e `resolve_entities()`.
- [ ] `pipeline.py` — Propagar `speaker_name` do `run_write_pipeline()` pra extract e entity_resolution.
- [ ] Atualizar testes existentes pra refletir novo comportamento.
- [ ] Adicionar testes específicos: speaker nomeado, fatos sobre o speaker, sem duplicação, sem mirror facts.

## Anti-scope

- **Não** implementar multi-speaker na mesma mensagem (v0 = 1 speaker por write call)
- **Não** mexer no read pipeline — funciona como está
- **Não** mexer no banco/migrations — mudança é na extração e resolução, não no schema
- **Não** mexer em background jobs (clustering, consolidation, memify)
- **Não** adicionar "memória do agente sobre si mesmo" (speaker diz "você é ótimo") — problema futuro
- **Não** fazer `speaker_name` opcional com fallback — é obrigatório, ponto. Dev que usa o SDK sabe quem tá falando.
- **Não** implementar merge retroativo de entidades `user:self` existentes — V0, não tem dados legados

## Technical Context

### Arquivos impactados (~4-5 arquivos core, ~100-150 linhas)
- `src/arandu/client.py` — assinatura do `write()`: adicionar `speaker_name: str`
- `src/arandu/write/extract.py` — 3 prompts + propagação do `speaker_name`
- `src/arandu/write/entity_resolution.py` — `_SELF_REFERENCES` → `_SPEAKER_REFERENCES`, resolver pro speaker
- `src/arandu/write/pipeline.py` — passar `speaker_name` pela orquestração
- `tests/` — `test_extract.py`, `test_entity_resolution.py`, `test_write_pipeline.py`

### Patterns a seguir
- Pipeline orchestration: manter extract → resolve → reconcile → upsert
- LLM interaction: prompts inline como constants, `temperature=0`, JSON response format
- Error handling: fail-safe — se extração falhar, retornar vazio
- Testing: mock-based com `FakeLLMProvider`
- Conventions: `snake_case`, type hints, Google-style docstrings

### O que NÃO muda
- `user_id` = dono da memória (agent). Intocado.
- Entity resolution 3-phase (exact → fuzzy → LLM). Intocada (só muda Phase 0).
- Semantic dedup pipeline. Intocado.
- Read pipeline. Intocado.
- Schema do banco (MemoryEntity, MemoryFact, etc.). Intocado.
- Background jobs. Intocados.

## Open Questions

Nenhuma. Discovery resolveu tudo:
- `speaker_name` é obrigatório (sem fallback anônimo)
- Sem backward compat com `user:self` (V0, sem usuários)
- Mirror facts eliminados completamente (fato pertence ao ator/possuidor, ponto)
