import type { ConfigManager } from '../../config/index.js'; import type { TurnEvent } from '../../../events/turn.js'; import type { VoiceService } from '../service.js'; import type { AudioSink } from './audio-sink.js'; export interface SpokenTurnControllerOptions { readonly voiceService: Pick; readonly configManager: Pick; /** The injectable I/O boundary. See {@link AudioSink}. */ readonly sink: AudioSink; readonly notify?: ((message: string) => void) | undefined; /** * Attribution label recorded in each synthesis request's metadata.source * (e.g. 'goodvibes-tui', 'goodvibes-agent', 'goodvibes-webui'). Defaults to * 'goodvibes-sdk'. */ readonly source?: string | undefined; readonly now?: (() => number) | undefined; readonly setInterval?: typeof setInterval | undefined; readonly clearInterval?: typeof clearInterval | undefined; /** Injectable clock for retry backoff timers (tests use fakes). */ readonly setTimeout?: typeof setTimeout | undefined; readonly clearTimeout?: typeof clearTimeout | undefined; } /** * SpokenTurnController, the shared spoken-output policy engine. It watches a * turn's lifecycle events, chunks the streamed answer into speech-sized pieces, * merges and dispatches synthesis requests through a bounded 2-slot window with * retry/backoff, and drives an injected {@link AudioSink} with honest * drain-vs-interrupt semantics. All I/O is the sink's; this class is pure * policy and runs unchanged in the terminal, the agent, and the browser. */ export declare class SpokenTurnController { private pendingPrompt; private activeTurnId; private chunker; private chunkSequence; private playbackChain; private readonly abortControllers; private timer; private errorReportedForTurn; private noSinkNoticed; /** Chunker output waiting to be merged into a synthesis request. */ private pendingTexts; /** Requests currently in the pipeline (synthesizing / waiting / playing). */ private pipelineDepth; /** Bumped on every teardown so stale pipeline releases are ignored. */ private pipelineGeneration; private pumpScheduled; /** Set when TURN_COMPLETED arrives; the turn releases once the pipeline drains. */ private completedTurnId; private readonly voiceService; private readonly configManager; private readonly sink; private readonly notify?; private readonly source; private readonly now; private readonly setIntervalImpl; private readonly clearIntervalImpl; private readonly setTimeoutImpl; private readonly clearTimeoutImpl; constructor(options: SpokenTurnControllerOptions); submitNextTurn(prompt: string): boolean; /** * Returns whether speech was actually ACTIVE when stopped. The notice only * prints in that case, stop() on an idle controller used to notify anyway, * spamming "[TTS] Spoken output stopped." on every Ctrl+C (an earlier replay * fix); callers use the return to decide whether the press "did a * job" (see handleCtrlC's consume-on-speech-stop). */ stop(message?: string): boolean; /** * Exit-path teardown: drops everything not yet audible (pending arm, * buffered text, queued chunks) but lets the audio the user is already * hearing finish naturally, capped at `drainTimeoutMs`, before the hard * stop. Deliberate interrupts (Ctrl+C, /tts stop, turn cancel) keep their * instant path through stop(); this is only for exiting the app while the * final audio of a completed response is still draining. */ stopForExit(drainTimeoutMs?: number): Promise; handleTurnEvent(event: TurnEvent): void; private maybeStartTurn; private finishTurn; private resetPipeline; private startTimer; private stopTimer; /** * Chunker output does NOT map 1:1 to synthesis requests. Text queues here * and the pump merges everything pending into one request whenever a * pipeline slot is free, so the request count tracks how often the model * out-paces the audio, not how many sentences it wrote. A short answer that * arrives before the first pump tick is exactly one request. */ private queueTexts; /** * Deferred one tick so text delivered in the same synchronous burst (fast * deltas, or a turn that completes instantly) coalesces into a single * request instead of firing per sentence boundary. */ private schedulePump; private pump; /** Merge everything pending into one request, capped at the per-request text limit. */ private takeMergedText; private dispatchChunk; private releasePipelineSlot; private maybeReleaseTurn; private synthesizeWithRetry; /** Abortable backoff sleep, an abort clears the timer and rejects, so a stop mid-backoff leaves nothing running. */ private delay; private synthesize; /** * One synthesis request failed after its retries. Report once per turn and * keep going, the rest of the response still plays. */ private reportSkippedChunk; private reportError; } //# sourceMappingURL=controller.d.ts.map