import { isEventIncludedInSnapshot, toClientAgentEvent, type AgentEventLike, } from "./agent-event-wire"; export interface AgentEventStreamSession { readonly isStreaming: boolean; readonly streamingMessage: unknown; onEvent(listener: (event: AgentEventLike) => void): () => void; } const HEARTBEAT_INTERVAL_MS = 30_000; function errorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } /** * Open the SSE transport immediately, then publish the session snapshot only * after the agent is ready and its event listener has been installed. */ export function createAgentEventStream( req: Request, sessionId: string, sessionPromise: Promise, ): ReadableStream { let cancelStream: (closeController: boolean) => void = () => {}; return new ReadableStream({ start(controller) { const encoder = new TextEncoder(); let closed = false; let heartbeat: ReturnType | null = null; let unsubscribe: (() => void) | null = null; let abortHandler: (() => void) | null = null; const cleanup = (closeController: boolean) => { if (closed) return; closed = true; if (heartbeat !== null) clearInterval(heartbeat); unsubscribe?.(); unsubscribe = null; if (abortHandler) req.signal.removeEventListener("abort", abortHandler); if (closeController) { try { controller.close(); } catch { /* stream already closed */ } } }; cancelStream = cleanup; const enqueueText = (text: string) => { if (closed) return; try { controller.enqueue(encoder.encode(text)); } catch { cleanup(false); } }; const encode = (data: unknown) => { enqueueText(`data: ${JSON.stringify(data)}\n\n`); }; const forwardEvent = (event: AgentEventLike, snapshot: unknown) => { if (isEventIncludedInSnapshot(event, snapshot)) return; const clientEvent = toClientAgentEvent(event); if (clientEvent) encode(clientEvent); }; const publishSession = async () => { try { const session = await sessionPromise; if (closed) return; const bufferedEvents: AgentEventLike[] = []; let snapshotPublished = false; const handleEvent = (event: AgentEventLike) => { if (!snapshotPublished) { bufferedEvents.push(event); return; } forwardEvent(event, snapshot); }; const stopListening = session.onEvent(handleEvent); if (closed) { stopListening(); return; } unsubscribe = stopListening; const snapshot = session.streamingMessage; encode({ type: "connected", sessionId, isStreaming: session.isStreaming, }); for (const event of bufferedEvents) forwardEvent(event, snapshot); if (snapshot !== undefined && snapshot !== null) { encode({ type: "message_start", message: snapshot }); } snapshotPublished = true; } catch (error) { if (closed) return; encode({ type: "startup_error", errorMessage: `Failed to start agent: ${errorMessage(error)}`, }); cleanup(true); } }; // Attach the rejection handler before checking the request signal. The // route may already have started a shared cold-start promise. void publishSession(); abortHandler = () => cleanup(true); if (req.signal.aborted) { cleanup(true); return; } req.signal.addEventListener("abort", abortHandler, { once: true }); heartbeat = setInterval(() => enqueueText(":\n\n"), HEARTBEAT_INTERVAL_MS); // Force the response headers through without claiming that the agent is // ready. The client waits for the later `connected` data event. enqueueText(":\n\n"); }, cancel() { cancelStream(false); }, }); }