profileShare

rasmusjy / roundtable

Read-only snapshot

No repository description.

main default branch 181 files Expires Sep 13, 2026, 9:06 AM
events.ts 3,163 bytes
1 /**
2 * The single typed event stream produced by the orchestrator.
3 *
4 * Both consumers subscribe to this one union:
5 * - the SSE endpoint serializes each event to the browser
6 * - the persistence layer reacts to `*_completed` events to write StageResults
7 *
8 * Keeping one source of truth means "what the UI sees" and "what we store" can
9 * never drift apart.
10 */
11 import type {
12 AnswerRecord,
13 ConvergenceRecord,
14 CritiqueRecord,
15 DebateStatus,
16 Participant,
17 ProvenanceRecord,
18 PublicDebateConfig,
19 RevisionRecord,
20 StageType,
21 SynthesisRecord,
22 } from './types';
23
24 export type DebateEvent =
25 | {
26 type: 'debate_started';
27 debateId: string;
28 config: PublicDebateConfig;
29 participants: Participant[];
30 chairmanModel: string;
31 /** True when chairman shares a provider family with a council member. */
32 chairmanProviderConflict: boolean;
33 }
34 | { type: 'round_started'; round: number; kind: 'answers' | 'cycle' }
35 | {
36 type: 'stage_started';
37 round: number;
38 stage: StageType;
39 participantId?: string;
40 model?: string;
41 }
42 | {
43 type: 'token_delta';
44 round: number;
45 stage: StageType;
46 participantId: string;
47 delta: string;
48 }
49 | { type: 'answer_completed'; round: number; record: AnswerRecord }
50 | { type: 'critique_completed'; round: number; record: CritiqueRecord }
51 | { type: 'revision_completed'; round: number; record: RevisionRecord }
52 | { type: 'convergence_result'; round: number; record: ConvergenceRecord }
53 | {
54 type: 'model_failed';
55 round: number;
56 stage: StageType;
57 participantId: string;
58 model: string;
59 error: string;
60 droppedFromDebate: boolean;
61 }
62 | { type: 'synthesis_completed'; record: SynthesisRecord }
63 | { type: 'provenance_completed'; record: ProvenanceRecord }
64 | {
65 /** Spend crossed the configured cap; skipping remaining rounds. */
66 type: 'budget_reached';
67 round: number;
68 totalCostUsd: number;
69 maxCostUsd: number;
70 }
71 | {
72 /**
73 * The user concluded the debate early: remaining rounds are skipped and
74 * synthesis runs on the answers as they stand. Live-only (not persisted).
75 */
76 type: 'gavel_struck';
77 round: number;
78 }
79 | {
80 type: 'cost_update';
81 totalCostUsd: number;
82 promptTokens: number;
83 completionTokens: number;
84 costByModel: Record<string, number>;
85 }
86 | {
87 type: 'debate_completed';
88 debateId: string;
89 status: DebateStatus;
90 totalCostUsd: number;
91 rounds: number;
92 durationMs: number;
93 }
94 | { type: 'debate_failed'; debateId: string; error: string };
95
96 export type DebateEventType = DebateEvent['type'];
97
98 /** Narrowing helper for consumers that only care about one event kind. */
99 export function isEvent<T extends DebateEventType>(
100 event: DebateEvent,
101 type: T,
102 ): event is Extract<DebateEvent, { type: T }> {
103 return event.type === type;
104 }
105
106 export type EmitFn = (event: DebateEvent) => void | Promise<void>;
107
108 /** A no-op emitter, handy in tests that don't assert on the stream. */
109 export const noopEmit: EmitFn = () => undefined;
110