/** * Search-box.ai * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * File: packages/events/src/index.ts * Description: Typed research event protocol streamed from backend to UI. */ import type { Claim, Contradiction, Evidence, SessionStatus, Source } from "@search-box/shared"; /** * Every UI update corresponds to exactly one of these events, persisted in * order (monotonic `seq`) so streams can be replayed via Last-Event-ID. */ export type ResearchEventPayload = | { type: "session.started"; question: string } | { type: "session.status"; status: SessionStatus } | { type: "session.completed"; answer: string } | { type: "session.failed"; error: string } | { type: "plan.updated"; objectives: string[]; publicReason: string } | { type: "thought"; publicReason: string } | { type: "action.started"; actionId: string; kind: "search" | "fetch"; label: string; input: Record; } | { type: "action.completed"; actionId: string; kind: "search" | "fetch"; ok: boolean; summary: string; latencyMs: number; } | { type: "source.added"; source: Source } | { type: "source.updated"; source: Source } | { type: "evidence.added"; evidence: Evidence; sourceUrl: string; sourceTitle: string | null } | { type: "claim.added"; claim: Claim } | { type: "claim.updated"; claim: Claim } | { type: "contradiction.added"; contradiction: Contradiction } | { type: "budget.updated"; usage: Record; limits: Record } | { type: "synthesis.started" } | { type: "answer.delta"; delta: string } | { type: "answer.completed"; answer: string; citations: CitationMapEntry[] }; export interface CitationMapEntry { index: number; sourceId: string; url: string; title: string | null; } export interface ResearchEvent { id: string; sessionId: string; seq: number; createdAt: string; payload: ResearchEventPayload; } export type ResearchEventType = ResearchEventPayload["type"];