import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; import type { CodexConversionConfig } from "../../adapter/activation/config.ts"; import type { CodexVoiceAuth } from "../auth.ts"; import type { RealtimeInitialMessageItem } from "../context.ts"; import type { CodexRealtimePeer } from "../conversation/peer.ts"; import type { CodexRealtimeConversation } from "../conversation/session.ts"; import type { RealtimeVoiceEventDetails } from "../conversation/wire.ts"; import type { CodexDictationSession } from "../dictation/session.ts"; import { rewriteTaskDelegation } from "../task-delegation.ts"; import type { RealtimeVoiceTurn } from "../turns.ts"; interface SessionLifecycle { stillAuthorizing(): boolean; onCreated(session: T): void; isCurrent(session: T): boolean; onActive(session: T): void; onError(session: T, error: Error): void; onStatus(status: string): void; } interface RealtimeSessionLifecycle extends SessionLifecycle { onEndCall(session: CodexRealtimeConversation): void; onDrop(session: CodexRealtimeConversation, error: Error): void; onTurn(session: CodexRealtimeConversation, turn: RealtimeVoiceTurn): void; onUserTranscript(transcript: string): void; onTranscriptTail(transcript: string): void; onEvent(event: RealtimeVoiceEventDetails): void; } interface DictationSessionLifecycle extends SessionLifecycle { onTranscript(transcript: string): void; } export async function startControllerConversation(options: { ctx: ExtensionContext; auth: CodexVoiceAuth; config: CodexConversionConfig; instructions: string; initialItems?: RealtimeInitialMessageItem[] | undefined; inputMuted?: boolean | undefined; greeting?: "fresh" | "contextual" | "handoff" | undefined; peer?: CodexRealtimePeer | undefined; signal?: AbortSignal | undefined; lifecycle: RealtimeSessionLifecycle; }): Promise { if (options.signal?.aborted) { await options.peer?.close(); return; } const { CodexRealtimeConversation } = await import("../conversation/session.ts"); if (!options.lifecycle.stillAuthorizing() || options.signal?.aborted) { await options.peer?.close(); return; } const realtimePeer = options.peer ?? new (await import("../conversation/native-peer.ts")).NativeCodexRealtimePeer(); if (!options.lifecycle.stillAuthorizing() || options.signal?.aborted) { await realtimePeer.close(); return; } let session!: CodexRealtimeConversation; session = new CodexRealtimeConversation({ onError: (error) => options.lifecycle.onError(session, error), onDrop: (error) => options.lifecycle.onDrop(session, error), onStatus: options.lifecycle.onStatus, onEndCall: () => options.lifecycle.onEndCall(session), onTurn: (turn) => options.lifecycle.onTurn(session, turn), onUserTranscript: options.lifecycle.onUserTranscript, onTranscriptTail: options.lifecycle.onTranscriptTail, rewriteDelegation: (turn, signal) => rewriteTaskDelegation(options.ctx, options.config.voice, turn, signal), onDelegationError: (error) => options.ctx.ui.notify(error.message, "error"), onEvent: options.lifecycle.onEvent, }, realtimePeer, options.config.voice.model !== "codex-live-1.1"); options.lifecycle.onCreated(session); if (options.signal?.aborted) { await session.close(); return; } const closeOnAbort = () => { void session.close(); }; options.signal?.addEventListener("abort", closeOnAbort, { once: true }); try { await session.start( options.auth, options.config, options.instructions, options.initialItems, options.inputMuted, ); } finally { options.signal?.removeEventListener("abort", closeOnAbort); } if (options.lifecycle.isCurrent(session)) { session.markEstablished(); options.lifecycle.onActive(session); if (options.greeting) { setTimeout(() => { if (options.lifecycle.isCurrent(session)) session.greet(options.greeting === "contextual", options.greeting === "handoff"); }, 0).unref?.(); } } else await session.close(); } export async function startControllerDictation(options: { auth: CodexVoiceAuth; config: CodexConversionConfig; lifecycle: DictationSessionLifecycle; }): Promise { const { CodexDictationSession } = await import("../dictation/session.ts"); if (!options.lifecycle.stillAuthorizing()) return; let session!: CodexDictationSession; session = new CodexDictationSession({ onError: (error) => options.lifecycle.onError(session, error), onStatus: options.lifecycle.onStatus, onTranscript: options.lifecycle.onTranscript, }); options.lifecycle.onCreated(session); await session.start(options.auth, options.config); if (options.lifecycle.isCurrent(session)) options.lifecycle.onActive(session); else await session.close(); }