from dataclasses import dataclass from enum import StrEnum class TurnState(StrEnum): LISTENING = "listening" PROCESSING = "processing" SPEAKING = "speaking" COMPLETED = "completed" INTERRUPTED = "interrupted" @dataclass(slots=True) class Turn: turn_id: int state: TurnState = TurnState.LISTENING partial_text: str = "" final_text: str | None = None class TurnManager: def __init__(self) -> None: self._active_turn: Turn | None = None @property def active_turn(self) -> Turn | None: return self._active_turn def start(self, turn_id: int) -> Turn: if self._active_turn is not None: raise RuntimeError( "A turn is already active." ) self._active_turn = Turn( turn_id=turn_id, state=TurnState.LISTENING, ) return self._active_turn def on_partial( self, turn_id: int, text: str, ) -> None: turn = self._require_active_turn(turn_id) if turn.state != TurnState.LISTENING: raise RuntimeError( f"Cannot receive partial transcript while " f"turn is {turn.state.value}." ) turn.partial_text = text def on_final( self, turn_id: int, text: str, ) -> None: turn = self._require_active_turn(turn_id) if turn.state != TurnState.LISTENING: raise RuntimeError( f"Cannot receive final transcript while " f"turn is {turn.state.value}." ) turn.final_text = text turn.partial_text = text turn.state = TurnState.PROCESSING def on_agent_response( self, turn_id: int, ) -> None: turn = self._require_active_turn(turn_id) if turn.state != TurnState.PROCESSING: raise RuntimeError( f"Cannot start speaking while " f"turn is {turn.state.value}." ) turn.state = TurnState.SPEAKING def on_tts_complete( self, turn_id: int, ) -> None: turn = self._require_active_turn(turn_id) if turn.state != TurnState.SPEAKING: raise RuntimeError( f"Cannot complete TTS while " f"turn is {turn.state.value}." ) turn.state = TurnState.COMPLETED def stop(self, turn_id: int) -> Turn: """ Return the active turn when its complete lifecycle has finished. A turn may only be stopped after the TTS stage has completed and the semantic state has reached COMPLETED. """ turn = self._require_active_turn(turn_id) if turn.state != TurnState.COMPLETED: raise RuntimeError( f"Cannot stop turn while turn is {turn.state.value}." ) return turn def interrupt(self, turn_id: int) -> Turn: turn = self._require_active_turn(turn_id) turn.state = TurnState.INTERRUPTED return turn def clear(self, turn_id: int) -> None: """ Clear the active turn after its lifecycle has been handled. Turn identity validation is delegated to _require_active_turn(). """ self._require_active_turn(turn_id) self._active_turn = None def _require_active_turn(self, turn_id: int) -> Turn: if self._active_turn is None: raise RuntimeError( "There is no active turn." ) if self._active_turn.turn_id != turn_id: raise ValueError( f"Unexpected turn_id={turn_id}; " f"active_turn_id={self._active_turn.turn_id}." ) return self._active_turn