---
adr: 001
title: "Chat pattern — substituir 'AG-UI style' caseiro pelo AG-UI v1 + artefatos tipados"
status: Proposed
date: 2026-05-17
owner: sa-principal
related_phase: Fase 4
---

# ADR-001 — Chat pattern: AG-UI v1 + artefatos tipados

## Status

Proposed — aguarda aprovação do `sa-principal` antes da Fase 4 iniciar.

## Context

O chat do Disclosure Bureau é o canal primário de uso do produto. Hoje a implementação está dividida entre três módulos:

- `/Users/guto/ufo/web/lib/chat/agui.ts` (65 LOC) — eventos SSE caseiros (`text_delta`, `tool_start`, `tool_result`, `navigate`, `done`, `error`).
- `/Users/guto/ufo/web/lib/chat/openrouter.ts` (341 LOC) — Pattern C (streaming + tool-call loop client-side) sobre OpenAI-compat da OpenRouter.
- `/Users/guto/ufo/web/components/chat-bubble.tsx` (507 LOC) — UI consumindo SSE manualmente.

O comentário no topo do `agui.ts` é explícito: _"We don't implement the full AG-UI protocol — we use a simplified event set"_. Isso traz limites concretos:

1. **Sem reconnect com Last-Event-ID** — queda de rede perde a mensagem inteira.
2. **Sem persistência incremental** — `text_delta` só vira `messages` row após `done`.
3. **Sem cancelamento limpo** — `AbortController` no client não chega ao server; tools continuam executando.
4. **Sem artefatos tipados** — citações são strings markdown parseadas no front, fáceis de errar.
5. **Sem crops bbox inline** — citação aponta para `/d/<doc>#<chunk>`, mas o crop só renderiza fora do chat.

Adicionalmente, `@assistant-ui/react@^0.14.0` está no `package.json` e **não é importado em runtime** (`grep -r "@assistant-ui" web/{app,components,lib}` → 0 hits). Decisão técnica adiada.

O brief lista esse problema como gap crítico #4: "A2UI / AG-UI no chat — streaming tool-calls, artefatos clicáveis, crops bbox inline, multi-turn persistente."

## Opções consideradas

### Opção A — Continuar com `agui.ts` caseiro, adicionar features

- Manter o protocolo proprietário, adicionar reconnect, artefatos tipados, cancelamento.
- **Custo:** ~6d.
- **Prós:** zero migração; controle total.
- **Contras:** reinventa AG-UI v1 (que já é open-spec e bem documentado); custo de manutenção cresce; ecossistema não conecta.

### Opção B — Adotar AG-UI v1 (protocolo aberto)

- Substituir o protocolo SSE proprietário pela spec [AG-UI v1](https://github.com/ag-ui-protocol/ag-ui): eventos `state.start`, `text.delta`, `tool.call`, `tool.result`, `artifact.created`, `state.end`, `error`. Reconnect via `Last-Event-ID`.
- Client: implementação manual (~150 LOC) ou biblioteca compatível (`@ag-ui/client` em early stage).
- **Custo:** ~5d.
- **Prós:** spec aberta e versionada; outros sistemas (Claude Code, Codex) podem virar clients do mesmo backend; alavanca pra MCP server na Fase 5.
- **Contras:** AG-UI v1 ainda é jovem; spec pode mudar (versionamos a versão exata no chat-runtime).

### Opção C — Adotar `@assistant-ui/react` + LangGraph runtime

- Usar a lib já no `package.json`. Trade-off: o `@assistant-ui/react` ofusca o protocolo SSE sob abstrações React; persistência fica acoplada à lib.
- **Custo:** ~4d.
- **Prós:** UI pronta (composer, mensagens, tool blocks); animações grátis.
- **Contras:** opinião pesada na arquitetura; integração com nossos tools custom + Pattern C custa; menos controle do SSE protocol.

## Decisão

**Adotar Opção B — AG-UI v1 com client implementado manualmente.**

Justificativas, em ordem de peso:

1. **Spec aberta.** AG-UI v1 é versionado e públicó. Não comemos ofuscação proprietária de UI lib.
2. **Alavanca para Fase 5 (MCP server).** Os mesmos artefatos tipados (`citation`, `crop_image`, `entity_card`) são reutilizados como artifacts MCP. Um único contrato serve chat e agent integrations.
3. **Backwards-compatível com nosso `tools.ts`.** Não muda a forma como tools são definidos, só como o stream é emitido.
4. **Controle do SSE protocol.** Reconnect, cancelamento, persistência incremental ficam em nosso código, não em deps.
5. **Remove `@assistant-ui/react` do `package.json`** — débito 2.2.13 fechado.

## Implementação

Esquema do contrato (versão 1.0):

```typescript
type AGUIEvent =
  | { kind: "state.start"; session_id: string; turn_id: string }
  | { kind: "text.delta"; turn_id: string; delta: string }
  | { kind: "tool.call"; turn_id: string; id: string; name: string; args: Record<string, unknown> }
  | { kind: "tool.result"; turn_id: string; id: string; result: unknown; duration_ms: number }
  | { kind: "artifact.created"; turn_id: string; artifact: Artifact }
  | { kind: "state.end"; turn_id: string; usage: { tokens_in: number; tokens_out: number } }
  | { kind: "error"; turn_id: string; code: string; message: string };

type Artifact =
  | { type: "citation"; chunk_id: string; doc_id: string; page: number; bbox: BBox }
  | { type: "crop_image"; src: string; alt_en: string; alt_pt: string }
  | { type: "entity_card"; class: EntityClass; id: string }
  | { type: "case_card"; case_id: string }
  | { type: "evidence_card"; evidence_id: string }
  | { type: "hypothesis_card"; hypothesis_id: string }
  | { type: "navigation_offer"; target: string; label_en: string; label_pt: string };
```

Persistência incremental: cada `text.delta` apenda em buffer; ao `state.end`, materializa em `messages`. Reconnect: `EventSource` com header `Last-Event-ID: <event_seq>`; server replay desde o último seq.

Cancelamento: `POST /api/chat/cancel { session_id }` flag em memória + `chat_sessions.cancel_requested=true`; loop de tool-call checa antes de cada turno.

## Consequences

### Positivas

- Reconnect resiliente em redes móveis ruins (Persona A/C frequentemente lê no celular).
- Artefatos tipados → menos bug de parsing de citação markdown.
- Crops inline no chat (`crop_image` artifact gerado quando tool retorna chunk com bbox).
- Mensagens "salvas em progresso" mesmo se o usuário fecha o modal.
- MCP server (Fase 5) usa o mesmo contrato de artifacts — economia de design.

### Negativas

- ~5d de engenharia (versus 0d se ficássemos no caseiro).
- AG-UI v1 spec pode evoluir; precisamos versionar `chat-runtime` com pinning de versão.
- `@assistant-ui/react` removido do package.json — qualquer demo que importou via copy/paste de docs externos quebra (provavelmente nenhum, mas verificar).

### Neutras

- Não muda `tools.ts` (12 handlers permanecem). Não muda `OpenRouter`/`Claude Code` providers — só muda o "frame" de eventos emitidos.

## Validação

- Smoke test: usuário envia "/test/disconnect" durante streaming → desconecta rede 5s → reconecta → mensagem continua sem perda.
- E2E: tool `hybrid_search` retorna 5 hits → 5 `artifact.created` `citation` aparecem inline no chat; cada citação clica → `/d/.../p007#c0042` com crop visível.

## Referências

- AG-UI spec: <https://github.com/ag-ui-protocol/ag-ui>
- Pattern C streaming: anthropic + openai cookbooks
- Issue: gap #4 do brief original
