1use std::fmt;
10use std::time::Instant;
11use tokio::sync::{broadcast, watch};
12
13use super::errors::SessionError;
14use super::events::{SessionEvent, Turn};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum SessionPhase {
19 Disconnected,
21 Connecting,
23 SetupSent,
25 Active,
27 UserSpeaking,
29 ModelSpeaking,
31 Interrupted,
33 ToolCallPending,
35 ToolCallExecuting,
37 Disconnecting,
39}
40
41impl fmt::Display for SessionPhase {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 match self {
44 Self::Disconnected => write!(f, "Disconnected"),
45 Self::Connecting => write!(f, "Connecting"),
46 Self::SetupSent => write!(f, "SetupSent"),
47 Self::Active => write!(f, "Active"),
48 Self::UserSpeaking => write!(f, "UserSpeaking"),
49 Self::ModelSpeaking => write!(f, "ModelSpeaking"),
50 Self::Interrupted => write!(f, "Interrupted"),
51 Self::ToolCallPending => write!(f, "ToolCallPending"),
52 Self::ToolCallExecuting => write!(f, "ToolCallExecuting"),
53 Self::Disconnecting => write!(f, "Disconnecting"),
54 }
55 }
56}
57
58impl SessionPhase {
59 pub fn can_transition_to(&self, to: &SessionPhase) -> bool {
61 matches!(
62 (self, to),
63 (SessionPhase::Disconnected, SessionPhase::Connecting)
65 | (SessionPhase::Connecting, SessionPhase::SetupSent)
66 | (SessionPhase::SetupSent, SessionPhase::Active)
67 | (SessionPhase::Active, SessionPhase::UserSpeaking)
69 | (SessionPhase::Active, SessionPhase::ModelSpeaking)
70 | (SessionPhase::Active, SessionPhase::ToolCallPending)
71 | (SessionPhase::UserSpeaking, SessionPhase::Active)
73 | (SessionPhase::UserSpeaking, SessionPhase::ModelSpeaking)
74 | (SessionPhase::ModelSpeaking, SessionPhase::Active)
76 | (SessionPhase::ModelSpeaking, SessionPhase::Interrupted)
77 | (SessionPhase::ModelSpeaking, SessionPhase::ToolCallPending)
78 | (SessionPhase::Interrupted, SessionPhase::Active)
80 | (SessionPhase::Interrupted, SessionPhase::UserSpeaking)
81 | (SessionPhase::ToolCallPending, SessionPhase::ToolCallExecuting)
83 | (SessionPhase::ToolCallExecuting, SessionPhase::Active)
84 | (SessionPhase::ToolCallExecuting, SessionPhase::ModelSpeaking)
85 | (SessionPhase::Active, SessionPhase::Disconnecting)
87 | (SessionPhase::UserSpeaking, SessionPhase::Disconnecting)
88 | (SessionPhase::ModelSpeaking, SessionPhase::Disconnecting)
89 | (SessionPhase::Interrupted, SessionPhase::Disconnecting)
90 | (SessionPhase::ToolCallPending, SessionPhase::Disconnecting)
91 | (SessionPhase::ToolCallExecuting, SessionPhase::Disconnecting)
92 | (SessionPhase::Disconnecting, SessionPhase::Disconnected)
93 | (_, SessionPhase::Disconnected)
95 )
96 }
97}
98
99#[derive(Debug)]
105pub struct SessionState {
106 phase_tx: watch::Sender<SessionPhase>,
108 event_tx: Option<broadcast::Sender<SessionEvent>>,
110 pub session_id: String,
112 pub resume_handle: parking_lot::Mutex<Option<String>>,
114 pub turns: parking_lot::Mutex<Vec<Turn>>,
116 pub current_turn: parking_lot::Mutex<Option<Turn>>,
118 text_from_transcription: std::sync::atomic::AtomicBool,
121}
122
123impl SessionState {
124 pub fn new(phase_tx: watch::Sender<SessionPhase>) -> Self {
126 Self {
127 phase_tx,
128 event_tx: None,
129 session_id: uuid::Uuid::new_v4().to_string(),
130 resume_handle: parking_lot::Mutex::new(None),
131 turns: parking_lot::Mutex::new(Vec::new()),
132 current_turn: parking_lot::Mutex::new(None),
133 text_from_transcription: std::sync::atomic::AtomicBool::new(false),
134 }
135 }
136
137 pub(crate) fn set_text_from_transcription(&self, enabled: bool) {
140 self.text_from_transcription
141 .store(enabled, std::sync::atomic::Ordering::Relaxed);
142 }
143
144 pub(crate) fn text_from_transcription(&self) -> bool {
145 self.text_from_transcription
146 .load(std::sync::atomic::Ordering::Relaxed)
147 }
148
149 pub fn with_events(
151 phase_tx: watch::Sender<SessionPhase>,
152 event_tx: broadcast::Sender<SessionEvent>,
153 ) -> Self {
154 Self {
155 phase_tx,
156 event_tx: Some(event_tx),
157 session_id: uuid::Uuid::new_v4().to_string(),
158 resume_handle: parking_lot::Mutex::new(None),
159 turns: parking_lot::Mutex::new(Vec::new()),
160 current_turn: parking_lot::Mutex::new(None),
161 text_from_transcription: std::sync::atomic::AtomicBool::new(false),
162 }
163 }
164
165 pub fn phase(&self) -> SessionPhase {
167 *self.phase_tx.borrow()
168 }
169
170 pub fn transition_to(&self, to: SessionPhase) -> Result<SessionPhase, SessionError> {
175 let from = self.phase();
176 if !from.can_transition_to(&to) {
177 return Err(SessionError::InvalidTransition { from, to });
178 }
179 self.phase_tx.send_replace(to);
180 if let Some(ref tx) = self.event_tx {
181 let _ = tx.send(SessionEvent::PhaseChanged(to));
182 }
183 Ok(to)
184 }
185
186 pub fn force_phase(&self, phase: SessionPhase) {
188 self.phase_tx.send_replace(phase);
189 }
190
191 pub fn start_turn(&self) {
193 let mut current = self.current_turn.lock();
194 if let Some(prev) = current.take() {
195 self.turns.lock().push(prev);
196 }
197 *current = Some(Turn::new());
198 }
199
200 pub fn append_text(&self, text: &str) {
202 if let Some(turn) = self.current_turn.lock().as_mut() {
203 turn.text.push_str(text);
204 }
205 }
206
207 pub fn mark_audio(&self) {
209 if let Some(turn) = self.current_turn.lock().as_mut() {
210 turn.has_audio = true;
211 }
212 }
213
214 pub fn complete_turn(&self) -> Option<Turn> {
216 let mut current = self.current_turn.lock();
217 if let Some(turn) = current.as_mut() {
218 turn.completed_at = Some(Instant::now());
219 }
220 let completed = current.take();
221 if let Some(ref t) = completed {
222 self.turns.lock().push(t.clone());
223 }
224 completed
225 }
226
227 pub fn interrupt_turn(&self) {
229 if let Some(turn) = self.current_turn.lock().as_mut() {
230 turn.interrupted = true;
231 turn.completed_at = Some(Instant::now());
232 }
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn valid_connection_lifecycle() {
242 assert!(SessionPhase::Disconnected.can_transition_to(&SessionPhase::Connecting));
243 assert!(SessionPhase::Connecting.can_transition_to(&SessionPhase::SetupSent));
244 assert!(SessionPhase::SetupSent.can_transition_to(&SessionPhase::Active));
245 }
246
247 #[test]
248 fn valid_conversation_flow() {
249 assert!(SessionPhase::Active.can_transition_to(&SessionPhase::UserSpeaking));
250 assert!(SessionPhase::Active.can_transition_to(&SessionPhase::ModelSpeaking));
251 assert!(SessionPhase::UserSpeaking.can_transition_to(&SessionPhase::Active));
252 assert!(SessionPhase::ModelSpeaking.can_transition_to(&SessionPhase::Active));
253 }
254
255 #[test]
256 fn valid_barge_in() {
257 assert!(SessionPhase::ModelSpeaking.can_transition_to(&SessionPhase::Interrupted));
258 assert!(SessionPhase::Interrupted.can_transition_to(&SessionPhase::Active));
259 assert!(SessionPhase::Interrupted.can_transition_to(&SessionPhase::UserSpeaking));
260 }
261
262 #[test]
263 fn valid_tool_flow() {
264 assert!(SessionPhase::Active.can_transition_to(&SessionPhase::ToolCallPending));
265 assert!(SessionPhase::ModelSpeaking.can_transition_to(&SessionPhase::ToolCallPending));
266 assert!(SessionPhase::ToolCallPending.can_transition_to(&SessionPhase::ToolCallExecuting));
267 assert!(SessionPhase::ToolCallExecuting.can_transition_to(&SessionPhase::Active));
268 assert!(SessionPhase::ToolCallExecuting.can_transition_to(&SessionPhase::ModelSpeaking));
269 }
270
271 #[test]
272 fn valid_disconnect_from_any() {
273 let phases = [
274 SessionPhase::Disconnected,
275 SessionPhase::Connecting,
276 SessionPhase::SetupSent,
277 SessionPhase::Active,
278 SessionPhase::UserSpeaking,
279 SessionPhase::ModelSpeaking,
280 SessionPhase::Interrupted,
281 SessionPhase::ToolCallPending,
282 SessionPhase::ToolCallExecuting,
283 SessionPhase::Disconnecting,
284 ];
285
286 for phase in &phases {
287 assert!(
288 phase.can_transition_to(&SessionPhase::Disconnected),
289 "{phase} should be able to force-disconnect"
290 );
291 }
292 }
293
294 #[test]
295 fn invalid_transitions() {
296 assert!(!SessionPhase::Disconnected.can_transition_to(&SessionPhase::Active));
297 assert!(!SessionPhase::Connecting.can_transition_to(&SessionPhase::Active));
298 assert!(!SessionPhase::Active.can_transition_to(&SessionPhase::SetupSent));
299 assert!(!SessionPhase::UserSpeaking.can_transition_to(&SessionPhase::ToolCallExecuting));
300 assert!(!SessionPhase::Disconnecting.can_transition_to(&SessionPhase::Active));
301 }
302
303 #[test]
304 fn display_impl() {
305 assert_eq!(format!("{}", SessionPhase::Active), "Active");
306 assert_eq!(format!("{}", SessionPhase::ModelSpeaking), "ModelSpeaking");
307 }
308}