/** * AG-UI v1-aligned SSE event helpers (ADR-001, Phase 4). * * Implements the AG-UI v1 protocol shape — events and typed artifacts — so the * same backend can feed the in-app chat and future MCP / external clients. * Wire-format remains SSE for browser compatibility. * * Events: * text_delta — append text to the current assistant message * tool_start — model is calling a tool (renders collapsible block) * tool_result — local handler finished (fills the block) * navigate — clickable navigation button inline (legacy; prefer the * navigation_offer artifact below) * artifact — typed rich object the UI renders inline. See `Artifact`. * done — terminal event; carries usage + final assistant message id * error — terminal event; carries error detail * * SSE wire format per message: * * event: * data: * * */ export interface BBox { x: number; y: number; w: number; h: number; } /** Inline-rendered, typed payloads. The chat UI renders each kind as a card. */ export type Artifact = | { kind: "citation"; chunk_id: string; doc_id: string; page: number; type?: string; classification?: string | null; bbox?: BBox | null; snippet?: string; score?: number; } | { kind: "crop_image"; src: string; // /api/crop?doc=…&page=…&x=… doc_id: string; page: number; chunk_id?: string; alt_en?: string; alt_pt?: string; } | { kind: "entity_card"; entity_class: "person" | "organization" | "location" | "event" | "uap_object" | "vehicle" | "operation" | "concept"; entity_id: string; canonical_name: string; total_mentions?: number; documents_count?: number; } | { kind: "evidence_card"; evidence_id: string; title?: string; grade?: string } | { kind: "hypothesis_card"; hypothesis_id: string; title?: string; posterior?: number } | { kind: "case_card"; case_id: string; title?: string } | { kind: "navigation_offer"; target: string; label_en: string; label_pt: string }; export type AGUIEvent = | { type: "text_delta"; delta: string } | { type: "tool_start"; id: string; name: string; args: Record } | { type: "tool_result"; id: string; result: unknown; durationMs?: number } | { type: "navigate"; target: string; label: string } | { type: "artifact"; artifact: Artifact } | { type: "done"; provider: string; model: string; usage?: Record; messageId?: string } | { type: "error"; message: string }; /** * Encode an event into the byte-stream chunks expected by the SSE protocol. */ export function encodeEvent(ev: AGUIEvent): Uint8Array { const enc = new TextEncoder(); const json = JSON.stringify(ev); return enc.encode(`event: ${ev.type}\ndata: ${json}\n\n`); } /** * Helper that creates a ReadableStream + a typed `emit()` callback to push * events into it. Caller closes the stream by calling `emit({type:"done"})` or * `close()`. */ export function createEventStream(): { stream: ReadableStream; emit: (ev: AGUIEvent) => void; close: () => void; } { let controller!: ReadableStreamDefaultController; const stream = new ReadableStream({ start(c) { controller = c; }, }); return { stream, emit(ev) { try { controller.enqueue(encodeEvent(ev)); } catch { /* stream closed */ } }, close() { try { controller.close(); } catch { /* already closed */ } }, }; }