# Research: Streaming híbrido en chat de conversación

**Feature**: `003-chat-streaming` | **Date**: 2026-05-24

## R1 — Transporte servidor → cliente

**Decision**: **Server-Sent Events (SSE)** sobre HTTP con `Content-Type: text/event-stream`.

**Rationale**: Flujo unidireccional (servidor emite estados y tokens). Compatible con Laravel `StreamedResponse`. Más simple que WebSockets para v1; no requiere infraestructura adicional.

**Alternatives considered**:
- **WebSockets** — rechazado: bidireccional innecesario; más complejidad en Laravel + Angular.
- **Long polling** — rechazado: peor latencia y más carga.
- **NDJSON chunked HTTP** — viable pero menos estándar que SSE para eventos tipados.

## R2 — Cliente Angular: fetch vs EventSource

**Decision**: **`fetch` + `ReadableStream`** con parser manual de líneas SSE.

**Rationale**: `EventSource` solo soporta GET y no permite header `Authorization: Bearer` de forma limpia. El endpoint es `POST` con body JSON `{ message }`. `HttpClient` de Angular no expone streaming de respuesta de forma ergonómica para POST.

**Alternatives considered**:
- **EventSource con token en query** — rechazado: expone token en URL/logs.
- **HttpClient observe events** — limitado para consumo incremental en versiones actuales; fetch es más directo.

## R3 — Extensión contrato LLM

**Decision**: Ampliar `LlmChatCompletionContract` con `chatStream(messages, callable $onChunk, options): string`.

**Rationale**: Constitution III exige adaptadores intercambiables. OpenAI soporta `stream: true` en chat completions; Gemini `streamGenerateContent`. El método retorna el texto completo al final para persistencia (FR-004).

**Alternatives considered**:
- **Generator PHP nativo** — más idiomático en PHP 8.2 pero rompe interfaz síncrona existente; callable es cambio mínimo.
- **Solo simular stream en backend** (enviar respuesta completa en un chunk) — rechazado para US2; no cumple FR-003 en chat general.

## R4 — Emisión de estados (streaming híbrido)

**Decision**: Interface `AgentStreamEmitterInterface` con métodos `status(phase, message)`, `chunk(string)`, `done(array)`, `error(string)`.

**Rationale**: Desacopla handlers del formato SSE. `AgentOrchestratorService` y handlers emiten fases sin conocer HTTP. Cubre FR-002 y SC-002 durante classify + SQL antes del primer token LLM.

**Fases estándar**:
| Phase | Mensaje default |
|-------|-----------------|
| `analyzing` | Analizando tu pregunta… |
| `querying` | Consultando datos… |
| `generating` | Preparando respuesta… |

**Alternatives considered**:
- **Solo stream LLM sin estados** — rechazado por acuerdo de producto (modo híbrido).

## R5 — Orden de persistencia

**Decision**:
1. Persistir mensaje **user** al iniciar el stream (antes de procesar).
2. Acumular chunks en memoria durante el stream.
3. Persistir mensaje **assistant** solo en evento `done` exitoso.
4. En **error** o desconexión: no persistir assistant parcial.

**Rationale**: FR-004, edge cases del spec (historial no corrupto). El user message ya queda guardado; retry no duplica user si el cliente reenvía (v2: idempotency key opcional).

**Alternatives considered**:
- **Persistir assistant al final de cada chunk** — rechazado: escrituras DB excesivas; riesgo de contenido parcial en crash.

## R6 — Alcance adjuntos

**Decision**: Mensajes con adjunto PDF/JPG siguen **`POST /api/v1/chat/message`** síncrono sin cambios.

**Rationale**: FR-006, acuerdo de producto. Extracción PDF + visión no beneficia de token streaming en v1.

## R7 — Markdown en UI v1

**Decision**: Durante stream mostrar **texto plano** acumulado; al evento `done` aplicar render Markdown (tablas/listas) si el contenido lo incluye.

**Rationale**: Assumption del spec. Renderizar Markdown parcial es frágil (tablas incompletas). Mejora UX post-stream sin bloquear v1.

**Alternatives considered**:
- **ngx-markdown en tiempo real** — posible fase 2; no requisito FR.

## R8 — Endpoint síncrono

**Decision**: Mantener `POST /api/v1/chat/message` para adjuntos y como **fallback** si `AGENT_STREAMING_ENABLED=false` o error de negociación cliente.

**Rationale**: FR-006, despliegue gradual, debugging.

## R9 — Buffering y proxies

**Decision**: En `StreamedResponse`, enviar headers:
- `Content-Type: text/event-stream`
- `Cache-Control: no-cache`
- `Connection: keep-alive`
- `X-Accel-Buffering: no`

Documentar en quickstart para nginx/Apache.

**Rationale**: Sin esto, SSE puede aparecer bloqueado hasta fin de respuesta, anulando SC-001.

## R10 — Handlers afectados

**Decision**: Implementar streaming en:
- `GeneralChatCapabilityHandler` — stream directo LLM
- `AgentDatabaseQueryCapabilityHandler` — status en consulta SQL + stream en interpretación (turno 2)

Otros handlers (`PriceUpdate`, etc.): emitir `generating` + stream o chunk único con respuesta completa.

**Rationale**: Capacidades habilitadas en `.env` típico: `general_chat` + `agent_database`.
