/** * Shared SSE event processor for the remote-server backend family. * * Both OpenCode and Kilo expose the same SSE wire format on * `global.event()` — upstream defines it once and the Kilo fork inherits * it. The per-event translation into stream-state mutations is therefore * identical between the two backends, so the logic lives here as a * shared helper instead of being duplicated in each backend's events * module. * * Two key functions: * * - {@link processStreamEvent} — translate one SSE event into state * mutations + callback firings. Returns a tag telling the caller * whether to keep iterating or stop. * - {@link finalizePartsIntoState} — drain the authoritative * `session.messages` parts list into stream state at end-of-turn, * filling anything SSE missed. * * Both are stateless beyond their `state` argument — easy to call with * a hand-built event object from a vitest test. * * Backend-specific extraction logic (synthetic-error detection, parts * walking) is injected as `extractPartsSummary` — both backends already * have an identical-signature implementation in their `sessions.ts`. */ import { appendText, closeCurrentSegment, markProgressDelivered, recordTokens, recordToolUse, recordToolCall, type StreamState, } from "../shared/index.js"; import { extractAssistantUsage, type RemoteAssistantInfo, } from "./session-helpers.js"; import { log, logDebug } from "../../util/log.js"; /** Format an error for a debug log line. */ function errMsg(err: unknown): string { return err instanceof Error ? err.message : String(err); } // ── Streaming timing ─────────────────────────────────────────────────────── /** * Minimum interval (ms) between `onStreamDelta` callbacks. Throttles * downstream UI refreshes (e.g. Telegram `edit_message`) so the frontend * doesn't spam the platform during fast-token generation. */ export const STREAM_INTERVAL_MS = 1000; // ── Types ────────────────────────────────────────────────────────────────── /** Tag returned by `processStreamEvent` describing what the loop should do. */ export type ProcessEventOutcome = | { kind: "continue" } | { kind: "stop"; reason: "turn.close" | "idle" | "out_of_scope" } | { kind: "terminator_fired"; toolName: string }; /** Common context passed to the per-event helper. */ export interface EventProcessingContext { /** Chat id for session-scoped metrics. */ chatId: string; /** Session id we're scoped to — events for other sessions are dropped. */ sessionId: string; /** Stream state accumulator (shared/). */ state: StreamState; /** Tool-call ids we've already fired callbacks for. */ seenToolCallIds: Set; /** Backend label used in `[