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}
119
120impl SessionState {
121 pub fn new(phase_tx: watch::Sender<SessionPhase>) -> Self {
123 Self {
124 phase_tx,
125 event_tx: None,
126 session_id: uuid::Uuid::new_v4().to_string(),
127 resume_handle: parking_lot::Mutex::new(None),
128 turns: parking_lot::Mutex::new(Vec::new()),
129 current_turn: parking_lot::Mutex::new(None),
130 }
131 }
132
133 pub fn with_events(
135 phase_tx: watch::Sender<SessionPhase>,
136 event_tx: broadcast::Sender<SessionEvent>,
137 ) -> Self {
138 Self {
139 phase_tx,
140 event_tx: Some(event_tx),
141 session_id: uuid::Uuid::new_v4().to_string(),
142 resume_handle: parking_lot::Mutex::new(None),
143 turns: parking_lot::Mutex::new(Vec::new()),
144 current_turn: parking_lot::Mutex::new(None),
145 }
146 }
147
148 pub fn phase(&self) -> SessionPhase {
150 *self.phase_tx.borrow()
151 }
152
153 pub fn transition_to(&self, to: SessionPhase) -> Result<SessionPhase, SessionError> {
158 let from = self.phase();
159 if !from.can_transition_to(&to) {
160 return Err(SessionError::InvalidTransition { from, to });
161 }
162 self.phase_tx.send_replace(to);
163 if let Some(ref tx) = self.event_tx {
164 let _ = tx.send(SessionEvent::PhaseChanged(to));
165 }
166 Ok(to)
167 }
168
169 pub fn force_phase(&self, phase: SessionPhase) {
171 self.phase_tx.send_replace(phase);
172 }
173
174 pub fn start_turn(&self) {
176 let mut current = self.current_turn.lock();
177 if let Some(prev) = current.take() {
178 self.turns.lock().push(prev);
179 }
180 *current = Some(Turn::new());
181 }
182
183 pub fn append_text(&self, text: &str) {
185 if let Some(turn) = self.current_turn.lock().as_mut() {
186 turn.text.push_str(text);
187 }
188 }
189
190 pub fn mark_audio(&self) {
192 if let Some(turn) = self.current_turn.lock().as_mut() {
193 turn.has_audio = true;
194 }
195 }
196
197 pub fn complete_turn(&self) -> Option<Turn> {
199 let mut current = self.current_turn.lock();
200 if let Some(turn) = current.as_mut() {
201 turn.completed_at = Some(Instant::now());
202 }
203 let completed = current.take();
204 if let Some(ref t) = completed {
205 self.turns.lock().push(t.clone());
206 }
207 completed
208 }
209
210 pub fn interrupt_turn(&self) {
212 if let Some(turn) = self.current_turn.lock().as_mut() {
213 turn.interrupted = true;
214 turn.completed_at = Some(Instant::now());
215 }
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn valid_connection_lifecycle() {
225 assert!(SessionPhase::Disconnected.can_transition_to(&SessionPhase::Connecting));
226 assert!(SessionPhase::Connecting.can_transition_to(&SessionPhase::SetupSent));
227 assert!(SessionPhase::SetupSent.can_transition_to(&SessionPhase::Active));
228 }
229
230 #[test]
231 fn valid_conversation_flow() {
232 assert!(SessionPhase::Active.can_transition_to(&SessionPhase::UserSpeaking));
233 assert!(SessionPhase::Active.can_transition_to(&SessionPhase::ModelSpeaking));
234 assert!(SessionPhase::UserSpeaking.can_transition_to(&SessionPhase::Active));
235 assert!(SessionPhase::ModelSpeaking.can_transition_to(&SessionPhase::Active));
236 }
237
238 #[test]
239 fn valid_barge_in() {
240 assert!(SessionPhase::ModelSpeaking.can_transition_to(&SessionPhase::Interrupted));
241 assert!(SessionPhase::Interrupted.can_transition_to(&SessionPhase::Active));
242 assert!(SessionPhase::Interrupted.can_transition_to(&SessionPhase::UserSpeaking));
243 }
244
245 #[test]
246 fn valid_tool_flow() {
247 assert!(SessionPhase::Active.can_transition_to(&SessionPhase::ToolCallPending));
248 assert!(SessionPhase::ModelSpeaking.can_transition_to(&SessionPhase::ToolCallPending));
249 assert!(SessionPhase::ToolCallPending.can_transition_to(&SessionPhase::ToolCallExecuting));
250 assert!(SessionPhase::ToolCallExecuting.can_transition_to(&SessionPhase::Active));
251 assert!(SessionPhase::ToolCallExecuting.can_transition_to(&SessionPhase::ModelSpeaking));
252 }
253
254 #[test]
255 fn valid_disconnect_from_any() {
256 let phases = [
257 SessionPhase::Disconnected,
258 SessionPhase::Connecting,
259 SessionPhase::SetupSent,
260 SessionPhase::Active,
261 SessionPhase::UserSpeaking,
262 SessionPhase::ModelSpeaking,
263 SessionPhase::Interrupted,
264 SessionPhase::ToolCallPending,
265 SessionPhase::ToolCallExecuting,
266 SessionPhase::Disconnecting,
267 ];
268
269 for phase in &phases {
270 assert!(
271 phase.can_transition_to(&SessionPhase::Disconnected),
272 "{phase} should be able to force-disconnect"
273 );
274 }
275 }
276
277 #[test]
278 fn invalid_transitions() {
279 assert!(!SessionPhase::Disconnected.can_transition_to(&SessionPhase::Active));
280 assert!(!SessionPhase::Connecting.can_transition_to(&SessionPhase::Active));
281 assert!(!SessionPhase::Active.can_transition_to(&SessionPhase::SetupSent));
282 assert!(!SessionPhase::UserSpeaking.can_transition_to(&SessionPhase::ToolCallExecuting));
283 assert!(!SessionPhase::Disconnecting.can_transition_to(&SessionPhase::Active));
284 }
285
286 #[test]
287 fn display_impl() {
288 assert_eq!(format!("{}", SessionPhase::Active), "Active");
289 assert_eq!(format!("{}", SessionPhase::ModelSpeaking), "ModelSpeaking");
290 }
291}