# Write Pipeline

Quando você chama `memory.write()`, o SDK lê uma mensagem em linguagem natural e automaticamente extrai quem e o que foi mencionado, descobre se é informação nova ou atualizada, e armazena tudo como fatos estruturados e versionados — numa única chamada.

**Você não precisa entender os internals pra usar.** Basta chamar `write()` e verificar `result.facts_added`. Esta página explica o que acontece por baixo dos panos, pra quando você quiser ajustar o comportamento ou debugar resultados.

```mermaid
flowchart LR
    A["Message"] --> B["Extract"]
    B --> C["Resolve Entities"]
    C --> D["Reconcile"]
    D --> E["Upsert"]
    E --> F["WriteResult"]
```

## Visão Geral

Toda chamada a `memory.write(user_id, message)` executa estes passos:

0. **Guard** — Mensagens vazias retornam imediatamente. Sem evento, sem chamada LLM, sem tokens consumidos.
1. **Registrar o evento** — A mensagem bruta é salva como trilha de auditoria imutável (nunca modificada ou deletada).
2. **Detectar emoção** — Classifica a emoção, intensidade e nível de energia da mensagem.
3. **Extrair** — Um LLM lê a mensagem e identifica pessoas, lugares, fatos e relacionamentos.
4. **Resolver entidades** — Deduplica menções ("Ana", "minha esposa Ana", "Aninha") em uma única entidade canônica.
5. **Reconciliar** — Compara cada fato novo contra o que já é conhecido. É novo? Uma atualização? Já conhecido? Uma retratação?
6. **Upsert** — Salva os resultados no banco de dados.

Cada estágio é independentemente fail-safe: se a extração falhar, o evento ainda é registrado. Se a reconciliação falhar para um fato, os outros prosseguem normalmente.

---

## Estágio 1: Extraction

**Em português claro:** O SDK manda sua mensagem pra um LLM e pergunta "Quais pessoas, lugares, fatos e relacionamentos são mencionados aqui?" O LLM retorna dados estruturados que o pipeline consegue trabalhar. Pense nisso como um parser inteligente que entende linguagem natural.

O estágio de extraction usa um LLM para transformar linguagem natural em dados estruturados: entidades, fatos e relacionamentos.

### Como Funciona

A extração roda 3 chamadas LLM, com as duas últimas concorrentes:

1. **Entity scan** — Identifica todas as entidades mencionadas na mensagem
2. **Fact extraction + Relation extraction** — Rodam concorrentemente via `asyncio.gather()`: fact extraction recebe todas as entidades numa única chamada, enquanto relation extraction identifica relacionamentos entre entidades

A extração de relations inclui um **retry automático**: se o LLM retornar 0 relations mas 2+ entities foram encontradas, o SDK repete a chamada uma vez. Quando `verbose=True`, o trace inclui `relation_retry_triggered`.

**Extração subject-centric:** Facts são extraídos do ponto de vista do subject principal apenas. "Carlos mora em Curitiba" é um fact sobre Carlos — o sistema NÃO cria "Curitiba é onde Carlos mora". O relacionamento `Carlos → lives_in → Curitiba` + entity links cuidam do retrieval cross-entity.

**Dedup semântico:** Após a extração, facts são comparados par a par por cosine similarity de embeddings. Near-duplicates (> 0.85 de similaridade) são removidos, mantendo a primeira ocorrência. Isso elimina reformulações cross-entity que o LLM às vezes produz apesar das instruções do prompt.

### Configuração

| Parâmetro | Default | Descrição |
|-----------|---------|-----------|
| `extraction_timeout_sec` | `30.0` | Timeout por chamada LLM |

### O Que é Extraído

Para cada mensagem, o estágio de extraction produz:

- **Entidades** — Coisas nomeadas: pessoas, organizações, lugares, conceitos, etc.
- **Fatos** — Afirmações auto-contidas sobre entidades em linguagem natural (ex: "Fernanda Lima é engenheira de software", "Marcos Tavares mora em Porto Alegre"). Cada texto de fato sempre inclui o nome da entidade — nunca apenas "é engenheira de software" sem um sujeito. Toda relação também gera um fato correspondente — então "Sarah é minha esposa" produz tanto uma relação (`user → spouse_of → sarah`) quanto um fato ("Sarah é esposa do user"). Fatos duplicados (mesmo subject + mesmo texto, ignorando pontuação) são removidos automaticamente pós-extração.
- **Relações** — Conexões entre entidades (ex: "Rafael" → `works_at` → "Acme Corp"). Relações servem como arestas de grafo pra traversal; o fato pareado torna a informação pesquisável via texto/embedding.

Cada fato inclui um **nível de confiança**:

| Nível | Score | Exemplo |
|-------|-------|---------|
| Declaração explícita | 0.95 | "Eu moro em São Paulo" |
| Inferência forte | 0.80 | "Fomos ao escritório de São Paulo" (implica localização) |
| Inferência fraca | 0.60 | Implicação contextual |
| Especulação | 0.40 | Informação incerta |

!!! note "Como a confiança funciona na prática"
    A confiança é atribuída pelo LLM durante a extração com base em como a informação foi declarada. Afirmações diretas ("Eu moro em SP") recebem alta confiança; afirmações incertas ("Acho que talvez...") recebem confiança mais baixa. Você não pode definir a confiança diretamente — ela é inferida. Você pode filtrar fatos de baixa confiança no retrieval usando `min_confidence` no MemoryConfig (padrão 0.55).

### Agrupamento de Aliases e Normalização de Subjects

Quando a mesma entidade é mencionada por múltiplos nomes em uma única mensagem (ex: "meu amigo Guili (Guilherme Maturana)"), a extração agrupa tudo em uma **única entidade com aliases** ao invés de criar duplicatas.

O LLM é instruído a escolher um nome canônico (geralmente o mais completo) e listar os outros como aliases:

```json
{
  "entities": [
    {"name": "Guilherme Maturana", "type": "person", "aliases": ["Guili"]}
  ]
}
```

Após a extração, um passo de **normalização de subjects** reescreve qualquer fato ou relação que referencia um alias para usar o nome canônico. Relações de identidade (ex: `same_as` entre um alias e seu nome canônico) são removidas automaticamente, já que se tornam auto-referenciantes após a normalização.

Isso elimina a duplicação de entidades intra-mensagem na origem — antes mesmo do entity resolution rodar.

### Entity Types

Entity types são **strings livres** — o LLM escolhe o tipo mais apropriado pra cada entidade. Tipos comuns incluem `person`, `organization`, `place`, `product`, `event`, `concept`, `pet`, mas qualquer tipo descritivo é aceito. Types são normalizados pra lowercase durante a entity resolution (ex: `"Person"` → `"person"`, `"PRODUCT"` → `"product"`).

O prompt de extração instrui o LLM a classificar types com cuidado — por exemplo, cidades são `place`, empresas são `organization`, produtos de software são `product`.

### Comportamento Fail-safe

Se uma chamada LLM falhar (timeout, JSON inválido, rate limit), a extraction retorna um resultado vazio em vez de lançar uma exceção. O evento ainda é registrado — nenhum dado é perdido. A próxima mensagem pode capturar a mesma informação.

!!! tip "Detectando timeouts"
    Quando a extração sofre timeout, o resultado é indistinguível de "mensagem sem conteúdo extraível" — 0 entidades, 0 fatos, sem exceção. Para detectar timeouts, compare o `duration_ms` da extração no trace com o `extraction_timeout_sec` configurado, ou verifique 0 entidades apesar de uma mensagem com conteúdo.

!!! info "Paralelo com neurociência"
    A extraction espelha a **codificação** na memória humana — o processo de converter input sensorial (uma conversa) em um traço de memória. Assim como a codificação humana é seletiva (não lembramos cada palavra), o LLM extrai apenas fatos e entidades salientes.

---

## Estágio 2: Entity Resolution

**Em português claro:** Quando alguém fala "Ana", "minha esposa Ana" e "Aninha" em mensagens diferentes, estão falando da mesma pessoa. Este estágio descobre isso e linka tudo a uma única entidade canônica — pra você não acabar com três registros separados de "Ana" no banco.

### Resolução em Três Fases

```mermaid
flowchart LR
    A["Entity name"] --> B{"Exact match?"}
    B -->|Yes| F["Resolved"]
    B -->|No| C{"Fuzzy match?"}
    C -->|"≥ 0.85"| F
    C -->|"0.50–0.85"| D{"LLM decides"}
    C -->|"< 0.50"| E["Create new entity"]
    D -->|Match| F
    D -->|No match| E
    E --> F
```

**Fase 1: Exact match**

Verifica o cache de aliases, slugs de entidades e display names. Instantâneo, sem chamada LLM.

Inclui **prefix/diminutive matching** para entidades do tipo pessoa: "Carol" corresponde a "Carolina" (mínimo 3 caracteres). Nota: "Jo" NÃO corresponde a "João" (< 3 chars). "Bob" corresponde a "Roberto" apenas se registrado como alias, não via prefix matching.

**Fase 2: Fuzzy match**

Usa similaridade de cosseno via embedding (in-memory) para encontrar candidatos:

- **≥ `fuzzy_threshold`** (default 0.85) — Match de alta confiança, resolve diretamente
- **0.50–`fuzzy_threshold`** — Ambíguo, encaminha top-3 candidatos para a Fase 3 (LLM)
- **< 0.50** — Sem match, cria uma nova entidade

Reduzir `fuzzy_threshold` expande a faixa de fuzzy-resolve e reduz chamadas LLM. Por exemplo, definir `fuzzy_threshold=0.50` elimina a faixa ambígua inteiramente — tudo acima de 0.50 resolve diretamente.

Faz fallback para `difflib.SequenceMatcher` quando embeddings não estão disponíveis.

**Fase 3: LLM fallback**

Envia candidatos ambíguos para o `LLMProvider` injetado para desambiguação. O LLM vê o nome da entidade, os candidatos, e decide qual (se algum) é um match.

??? example "Passo a passo: como a entity resolution funciona"
    **Mensagem:** "Falei com o Guili sobre o projeto. O Guilherme disse que tá no prazo."

    1. **Extração:** Dois nomes encontrados: "Guili" e "Guilherme"
    2. **Fase 1 (exata):** "Guilherme" bate com a entidade existente `person:guilherme_maturana`
    3. **Fase 2 (fuzzy):** "Guili" tem 0.87 de similaridade com "Guilherme" → resolve automaticamente
    4. **Resultado:** Ambos os nomes resolvem para a mesma entidade. Alias "Guili" registrado.

    Da próxima vez que "Guili" aparecer, a Fase 1 resolve instantaneamente via cache de aliases — sem precisar de fuzzy ou LLM.

### Casos Especiais

- **Auto-referências** — "I", "me", "eu", "myself" resolvem automaticamente para `user:self`
- **Termos de relacionamento** — "my girlfriend", "my brother", "meu amigo" resolvem para `user:self` (o relacionamento é sobre o usuário, não uma entidade separada)
- **Dicas relacionais** — `"Carol (namorada do Rafael)"` remove a dica e força `type="person"`

### Registro de Aliases

Quando um novo alias é descoberto (ex: "Aninha" resolve para `person:ana`), ele é registrado em `MemoryEntityAlias` com semântica **first-write-wins** — escritas concorrentes não criam aliases conflitantes. Aliases são scoped por `user_id`: o mesmo alias pode mapear para entidades diferentes para usuários diferentes.

**Aliases da extração** também são registrados automaticamente: quando o entity resolution cria uma nova entidade que tem aliases da extração (ex: "Guili" para "Guilherme Maturana"), todos os aliases são registrados em `MemoryEntityAlias` e adicionados ao cache de aliases in-memory. Isso significa que entidades subsequentes no mesmo batch podem resolver imediatamente via Fase 1 exact match — sem chamadas fuzzy ou LLM.

Isso cria uma defesa em duas linhas contra duplicatas:

1. **Intra-mensagem** — Agrupamento de aliases na extração previne duplicatas dentro de uma mesma mensagem
2. **Cross-mensagem** — Aliases registrados habilitam exact match em mensagens futuras (ex: se mensagem 1 cria "Guilherme Maturana" com alias "Guili", mensagem 2 mencionando "Guili" resolve instantaneamente via Fase 1)

### Persistência de Entidades

Após o entity resolution completar, o pipeline garante que **toda entidade resolvida** tenha um row na tabela `memory_entities` — não apenas as recém-criadas. Entidades resolvidas via exact match, fuzzy match ou LLM também recebem upsert via `ON CONFLICT DO UPDATE` (idempotente).

Isso é crítico porque background jobs (importance scoring, summary refresh, spreading activation) leem de `memory_entities`. Sem um row, esses jobs ficam cegos à entidade e não operam sobre ela.

O upsert de entidades é fail-safe: se uma entidade falhar ao persistir (ex: constraint violation), as outras prosseguem normalmente e o pipeline continua.

### Configuração

| Parâmetro | Default | Descrição |
|-----------|---------|-----------|
| `fuzzy_threshold` | `0.85` | Threshold de similaridade de cosseno para fuzzy match direto |
| `enable_llm_resolution` | `True` | Se deve usar LLM para casos ambíguos. Quando `False`, candidatos ambíguos criam uma nova entidade ao invés de chamar LLM. |

!!! note "Seleção de modelo"
    O modelo LLM usado para entity resolution é determinado pelo `LLMProvider` injetado no `MemoryClient`. Para usar um modelo diferente para resolução vs. extração, injete providers diferentes.

!!! info "Paralelo com neurociência"
    A entity resolution espelha a **memória associativa** — a capacidade do cérebro de vincular novos estímulos a representações existentes. Ouvir "Carol" ativa o padrão neural de "Carolina" através de pattern completion, assim como o fuzzy matching ativa entidades candidatas através de similaridade de embedding.

---

## Estágio 3: Reconciliação

**Em português claro:** Se o usuário disse "Eu moro em São Paulo" na semana passada e agora diz "Me mudei pro Rio", o sistema precisa entender que isso é uma atualização, não uma segunda casa. Este estágio compara cada fato novo contra o que já tá armazenado e decide: é informação nova? Atualização de algo existente? Já conhecido? Ou uma retratação?

### Lógica de Decisão

Para cada fato extraído, o reconciler:

1. **Busca fatos existentes** para a mesma entidade
2. **Calcula similaridade** entre o novo fato e cada fato existente (via embeddings)
3. **Decide a ação**:

| Ação | Quando | Exemplo |
|------|--------|---------|
| **ADD** | Informação nova, sem fato existente similar (similaridade < 0.50) | "fala francês" quando não existe fato de idioma |
| **UPDATE** | Substitui um fato existente (similaridade 0.50-0.85+) | "mora no Rio" substitui "mora em São Paulo" |
| **NOOP** | Já é conhecido (alta similaridade) | "trabalha na Acme" quando esse fato já existe |
| **DELETE** | Retrata explicitamente um fato | "Não trabalho mais na Acme" |

### Performance da Reconciliação

- **Fast path (similaridade < 0.50):** Auto-ADD sem chamada LLM (~300ms). Caminho comum para informação nova.
- **Slow path (similaridade ≥ 0.50):** LLM avalia se deve ADD, UPDATE, DELETE ou NOOP (~2-3s). Requer chamada LLM com contexto completo.

Planeje adequadamente: importações em massa de dados novos são rápidas; atualizações de conhecimento existente requerem decisão do LLM.

!!! note "Chains de UPDATE podem ramificar"
    O LLM de reconciliação pode escolher ADD ao invés de UPDATE quando interpreta a nova informação como distinta, não como substituição. Por exemplo, "me mudei pra BH" pode criar fatos separados para "mora em BH" e "morou no RJ" ao invés de um simples update chain. Isso preserva mais informação mas pode quebrar a chain de `supersedes_fact_id`. Este é o comportamento esperado — o LLM prioriza preservação de informação.

### Comportamento Fail-safe

Se a chamada LLM de reconciliação falhar, o sistema faz default para **ADD** — é melhor ter uma quase-duplicata do que perder informação. Os jobs de consolidation em background (clustering, deduplicação) limpam duplicatas depois.

### Versionamento de Fatos

Fatos são versionados usando janelas de validade temporal (`valid_from`, `valid_to`):

- **Fatos ativos** têm `valid_to = NULL`
- **Fatos atualizados** recebem tanto `valid_to` quanto `invalidated_at` definidos, e um novo fato é criado com `supersedes_fact_id` apontando para o antigo
- **Fatos deletados** recebem tanto `valid_to` quanto `invalidated_at` definidos

Isso permite queries de time-travel: você pode perguntar o que o sistema sabia em qualquer momento no tempo.

!!! info "Paralelo com neurociência"
    A reconciliação espelha a **reconsolidação** — o processo pelo qual memórias recuperadas se tornam lábeis e podem ser modificadas. Quando você recupera uma memória ("mora em São Paulo") e encontra nova informação ("acabou de se mudar pro Rio"), a memória original é atualizada. O cérebro não simplesmente sobrescreve — ele cria um novo traço ligado ao original, assim como UPDATE cria um novo fato com `supersedes_fact_id`.

---

## Estágio 4: Upsert

**Em português claro:** Aqui as decisões do estágio anterior são de fato salvas no banco. Fatos novos são inseridos, fatos desatualizados são marcados como substituídos, e relacionamentos entre entidades são criados ou reforçados. Tudo roda dentro de uma transação — se um fato falhar ao salvar, os outros ainda passam.

O estágio de upsert executa as decisões de reconciliação no banco de dados:

| Decisão | Ação no banco |
|---------|---------------|
| ADD | Cria novo `MemoryFact` com embedding |
| UPDATE | Fecha fato antigo (`valid_to = now`), cria novo com `supersedes_fact_id` |
| NOOP | Atualiza `last_confirmed_at` no fato existente |
| DELETE | Fecha fato (`valid_to = now`, `invalidated_at = now`) |

### Fact-Entity Links

Após cada fato ser persistido (ADD ou UPDATE), o pipeline cria **entity links** conectando o fato a todas as entidades que ele menciona — não só ao subject primário. Isso permite retrieval cross-entity sem duplicar fatos.

Por exemplo, "Clara Rezende saiu da Vertix" é armazenado **uma vez** com `entity_key = person:clara_rezende` (subject primário). Mas entity links são criados tanto pra `person:clara_rezende` (primário) quanto pra `organization:vertix` (secundário). Quando se busca sobre Vertix, o sistema encontra esse fato via link — sem necessidade de fato duplicado.

Links são criados por match de display names contra o texto do fato (case-insensitive substring match). Nomes muito curtos (< 3 caracteres) são pulados pra evitar false positives. A criação de links é fail-safe: se falhar, o fato persiste normalmente.

### Rastreamento de Relacionamentos

Durante o upsert, relacionamentos extraídos também são persistidos:

- Cria/atualiza registros `MemoryEntityRelationship`
- Resolve entidades de origem e destino via entity map
- **Reforço de strength**: relacionamentos repetidos aumentam `strength` (inicial: 0.8, reforçado até 1.0 ao longo de múltiplas mensagens)
- Usa `ON CONFLICT DO UPDATE` para upserts idempotentes

!!! warning "Relacionamentos são **unidirecionais**"
    Escrever "Ana trabalha na Acme" cria `ana → works_at → acme_corp`, mas **não** `acme_corp → employs → ana`. Isso significa que o graph retrieval começando por "Acme Corp" não vai encontrar Ana via relacionamentos (mas pode encontrá-la via similaridade semântica). Para criar ambas as direções, mencione-as explicitamente: "Ana trabalha na Acme. A Acme tem a Ana como data scientist."

#### Vinculação de Evidência e Cascata de Invalidação

**O problema:** Sem vinculação entre fatos e relacionamentos, arestas contraditórias se acumulam. Se um usuário diz "moro em Curitiba" e depois "me mudei pra São Paulo", o relacionamento antigo `user --[lives_in]--> curitiba` continuaria ativo junto com o novo — poluindo o retrieval com contexto obsoleto.

**A solução:** Cada relacionamento é vinculado ao fato que o sustenta via `evidence_fact_id`. Quando esse fato é substituído (UPDATE) ou retratado (DELETE), o relacionamento é **automaticamente invalidado** — sem necessidade de limpeza manual.

Como a vinculação de evidência funciona:

1. Após os fatos serem persistidos no estágio de upsert, um match heurístico associa cada relacionamento a um fato correspondente. Para um relacionamento `(source, target)`, o matcher busca fatos cujo `fact_text` menciona ambos os nomes de entidade.
2. Se múltiplos fatos fizerem match, o de maior confidence é selecionado.
3. O ID do fato é armazenado como `evidence_fact_id` no relacionamento.

Quando um fato é invalidado (via UPDATE ou DELETE), a **cascata de invalidação** automaticamente seta `invalidated_at` e `valid_to` em todos os relacionamentos que o referenciam. O [BFS do graph retrieval](../advanced/read-api.md) já filtra relacionamentos invalidados, então arestas obsoletas são imediatamente excluídas do contexto.

```
Usuário: "Moro em Curitiba"
  → fato: "User mora em Curitiba" (fact_1)
  → rel:  user --[lives_in]--> curitiba (evidence_fact_id = fact_1)

Usuário: "Me mudei pra São Paulo"
  → reconciliação: UPDATE fact_1 → fact_2 "User mora em São Paulo"
  → cascata: rel lives_in→curitiba INVALIDADA (evidence_fact_id = fact_1)
  → nova rel: user --[lives_in]--> sao_paulo (evidence_fact_id = fact_2)
```

!!! note "Tipos de relacionamento são dinâmicos"
    O campo `rel_type` aceita qualquer string descritiva em `snake_case` — não apenas um conjunto fixo. Tipos comuns incluem `works_at`, `lives_in`, `family_of`, mas o LLM também pode produzir tipos como `mentored_by` ou `inspired_by`. Veja [Tipos de Relacionamento Dinâmicos](../advanced/data-types.md#tipos-de-relacionamento-dinamicos) para detalhes sobre normalização e aliases.

#### Fatos-espelho

Às vezes o LLM infere um relacionamento a partir do contexto sem extrair um fato correspondente. Por exemplo, "vou pra Curitiba ver minha mãe" implica `mãe --[lives_in]--> curitiba`, mas o LLM pode extrair apenas um fato sobre a viagem do usuário — não sobre onde a mãe mora. Sem um fato, o relacionamento não participa da cascata de invalidação e não é encontrável via busca semântica.

Para resolver isso, um **fato-espelho** é criado automaticamente como fallback quando nenhum match heurístico é encontrado. O fato-espelho é uma frase simples em linguagem natural gerada a partir do relacionamento: `"{source_name} {rel_type} {target_name}"` (ex: `"Mom lives in Curitiba"`).

Fatos-espelho são marcados com:

- `confidence = 0.60` (inferência fraca — prioridade menor no ranking de retrieval)
- `source_context = "inferred_from_relation"` (permite filtrar ou reduzir prioridade se necessário)

!!! note "Mirror facts podem ficar stale"
    Mirror facts não são automaticamente invalidados quando o relacionamento de origem é removido. Eles podem persistir como dados obsoletos. Aplicações devem considerar filtrar por `source_context` quando a acurácia é crítica.

O ID do fato-espelho é usado como `evidence_fact_id` do relacionamento, então a cascata de invalidação funciona também para relacionamentos inferidos.

!!! tip "Reduzindo fatos-espelho"
    Os prompts de extração instruem o LLM a extrair fatos implícitos junto com relacionamentos (ex: "minha mãe mora em Curitiba" como fato, não apenas como relação). Conforme a extração LLM melhora, menos fatos-espelho são necessários — eles são a rede de segurança, não o mecanismo principal.

### Segurança Transacional

O write pipeline inteiro roda dentro de uma transação do banco de dados. Upserts individuais de fatos usam **savepoints** (`session.begin_nested()`) para que uma falha em um fato não aborte o batch inteiro:

```python
# Se este fato falhar, apenas este savepoint faz rollback
async with session.begin_nested():
    session.add(new_fact)
    await session.flush()
```

O registro do evento é criado e flushed primeiro, então ele sobrevive mesmo se todos os estágios subsequentes falharem.

---

## WriteResult

Após o pipeline completar, você recebe um `WriteResult` com observabilidade total:

```python
result = await memory.write(
    user_id="user_123",
    message="...",
    recent_messages=["mensagem anterior para resolução de pronomes"],  # opcional
)

# O que aconteceu
print(result.facts_added)       # Lista de fatos criados
print(result.facts_updated)     # Lista de fatos substituídos
print(result.facts_unchanged)   # Lista de fatos confirmados (decisões NOOP)
print(result.facts_deleted)     # Lista de fatos retratados (decisões DELETE)
print(result.entities_resolved) # Lista de entidades resolvidas
print(result.duration_ms)       # Tempo total do pipeline
print(result.event_id)          # ID único do evento para esta escrita
print(result.tokens_used)       # TokenUsage(input_tokens=..., output_tokens=..., total_tokens=...)
print(result.pipeline)          # PipelineTrace (quando verbose=True)
print(result.success)           # True se o pipeline completou sem erros (sempre verifique)
print(result.error)             # Mensagem de erro se o pipeline falhou (None em sucesso)
```

### Enriquecimento do Trace (verbose=True)

Quando `verbose=True`, o step de extraction no `PipelineTrace` inclui metadados adicionais:

| Campo | Tipo | Descrição |
|-------|------|-----------|
| `relation_retry_triggered` | `bool` | Se o retry automático de relations foi utilizado |

```python
result = await memory.write(user_id, message, verbose=True)
extraction_step = result.pipeline.steps[0]  # "extraction"
print(extraction_step.data["relation_retry_triggered"])  # True/False
```

### Token Usage

`tokens_used` reporta o total de tokens LLM consumidos em todas as chamadas do pipeline (extraction, entity resolution, reconciliation). Útil pra benchmarking e estimativa de custo.

```python
result = await memory.write(user_id, message)
print(result.tokens_used.input_tokens)   # ex: 1200
print(result.tokens_used.output_tokens)  # ex: 350
print(result.tokens_used.total_tokens)   # ex: 1550
```

!!! note "Token tracking requer suporte do provider"
    `tokens_used` é populado a partir de `LLMResult.usage` retornado pelo seu `LLMProvider`. O `OpenAIProvider` built-in reporta usage automaticamente. Providers customizados que retornam `LLMResult(text=..., usage=None)` mostrarão zero tokens.

### Config Overrides

Sobrescreva qualquer campo do `MemoryConfig` pra uma única chamada `write()` sem criar novo client:

```python
result = await memory.write(
    user_id="user_123",
    message="...",
    config_overrides={"extraction_timeout_sec": 60.0},
)
```

Só as chaves fornecidas são alteradas; todas as outras herdam do config do client. Chaves inválidas emitem warning e são ignoradas. Incompatibilidade de tipo levanta `ValueError`.

### Dry Run

Rode extraction sem persistir nada no banco:

```python
result = await memory.write(
    user_id="user_123",
    message="Eu moro em São Paulo com minha esposa Ana",
    dry_run=True,
)
# result.facts_added contém o que SERIA extraído
# result.tokens_used mostra o custo dessa extraction
# Nenhum evento, fato ou entidade é persistido
```

Útil pra benchmarking: rode a mesma mensagem com `dry_run=True` e compare `tokens_used` entre configurações diferentes.

---

## Diagrama do Pipeline (Completo)

```mermaid
flowchart TD
    MSG["User message"] --> EVT["Create MemoryEvent\n(immutable log + embedding)"]
    EVT --> EXT["Extraction\n(Entity Scan → Facts → Relations)"]
    EXT --> DEDUP["Semantic Dedup\n(remove near-duplicates)"]
    DEDUP --> RES["Entity Resolution\n(exact → fuzzy → LLM)"]
    RES --> REC["Reconciliation\n(ADD / UPDATE / NOOP / DELETE)"]
    REC --> UPS["Upsert + Entity Links\n(with savepoints)"]
    UPS --> REL["Relationship Tracking\n(strength reinforcement)"]
    REL --> WR["WriteResult"]
```
