1use std::sync::Arc;
11use std::time::Instant;
12
13use parking_lot::Mutex;
14use tokio::time::{Duration, Instant as TokioInstant, Interval};
15use tokio_util::sync::CancellationToken;
16
17pub use zeph_config::autonomous::AutonomousState;
18
19#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
21pub struct SupervisorVerdict {
22 pub achieved: bool,
24 pub reasoning: String,
26 pub confidence: f32,
28 pub suggestions: Vec<String>,
30}
31
32pub struct AutonomousSession {
36 pub goal_id: String,
38 pub goal_text: String,
40 pub state: AutonomousState,
42 pub turns_executed: u32,
44 pub max_turns: u32,
46 pub stuck_count: u32,
48 pub supervisor_fail_count: u32,
50 pub cancel: CancellationToken,
52 pub last_verdict: Option<SupervisorVerdict>,
54 pub started_at: Instant,
56 pub supervisor_retry_at: Option<TokioInstant>,
58}
59
60impl AutonomousSession {
61 #[must_use]
63 pub fn new(goal_id: impl Into<String>, goal_text: impl Into<String>, max_turns: u32) -> Self {
64 Self {
65 goal_id: goal_id.into(),
66 goal_text: goal_text.into(),
67 state: AutonomousState::Running,
68 turns_executed: 0,
69 max_turns,
70 stuck_count: 0,
71 supervisor_fail_count: 0,
72 cancel: CancellationToken::new(),
73 last_verdict: None,
74 started_at: Instant::now(),
75 supervisor_retry_at: None,
76 }
77 }
78
79 #[must_use]
81 pub fn is_terminal(&self) -> bool {
82 matches!(
83 self.state,
84 AutonomousState::Achieved
85 | AutonomousState::Stuck
86 | AutonomousState::Aborted
87 | AutonomousState::Failed
88 )
89 }
90
91 #[must_use]
93 pub fn elapsed(&self) -> Duration {
94 self.started_at.elapsed()
95 }
96}
97
98pub struct AutonomousDriver {
108 pub session: Option<AutonomousSession>,
110 turn_delay: Duration,
112 pub(crate) turn_interval: Option<Interval>,
114 pub pending_start_arc: Arc<Mutex<Option<(String, String, u32)>>>,
121}
122
123impl AutonomousDriver {
124 #[must_use]
132 pub fn new(turn_delay: Duration) -> Self {
133 assert!(
134 !turn_delay.is_zero(),
135 "autonomous turn delay must be non-zero"
136 );
137 Self {
138 session: None,
139 turn_delay,
140 turn_interval: None,
141 pending_start_arc: Arc::new(Mutex::new(None)),
142 }
143 }
144
145 #[must_use]
153 pub fn flush_pending_start(&mut self) -> Option<(Option<String>, String)> {
154 let pending = self.pending_start_arc.lock().take()?;
155 let (goal_id, goal_text, max_turns) = pending;
156 let new_id = goal_id.clone();
157 let cancelled = self.start_session(goal_id, goal_text, max_turns);
158 Some((cancelled, new_id))
159 }
160
161 #[must_use]
163 pub fn should_tick(&self) -> bool {
164 self.session
165 .as_ref()
166 .is_some_and(|s| s.state == AutonomousState::Running)
167 }
168
169 pub async fn next_tick(&mut self) {
176 let interval = self.turn_interval.get_or_insert_with(|| {
177 let mut iv = tokio::time::interval(self.turn_delay);
178 iv.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
179 iv
180 });
181 interval.tick().await;
182 }
183
184 pub fn start_session(
189 &mut self,
190 goal_id: impl Into<String>,
191 goal_text: impl Into<String>,
192 max_turns: u32,
193 ) -> Option<String> {
194 let prev = self.session.take();
195 let cancelled_id = prev.map(|s| {
196 s.cancel.cancel();
197 s.goal_id
198 });
199 self.session = Some(AutonomousSession::new(goal_id, goal_text, max_turns));
200 cancelled_id
201 }
202
203 pub fn abort(&mut self) -> Option<String> {
207 let s = self.session.as_mut()?;
208 s.cancel.cancel();
209 s.state = AutonomousState::Aborted;
210 let id = s.goal_id.clone();
211 self.session = None;
212 Some(id)
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn initial_state_is_running() {
222 let s = AutonomousSession::new("id1", "do something", 10);
223 assert_eq!(s.state, AutonomousState::Running);
224 assert_eq!(s.turns_executed, 0);
225 assert!(!s.is_terminal());
226 }
227
228 #[test]
229 fn terminal_states() {
230 for terminal in [
231 AutonomousState::Achieved,
232 AutonomousState::Stuck,
233 AutonomousState::Aborted,
234 AutonomousState::Failed,
235 ] {
236 let mut s = AutonomousSession::new("id", "text", 5);
237 s.state = terminal;
238 assert!(s.is_terminal(), "{terminal} should be terminal");
239 }
240 }
241
242 #[test]
243 fn running_and_verifying_are_not_terminal() {
244 for non_terminal in [AutonomousState::Running, AutonomousState::Verifying] {
245 let mut s = AutonomousSession::new("id", "text", 5);
246 s.state = non_terminal;
247 assert!(!s.is_terminal(), "{non_terminal} should not be terminal");
248 }
249 }
250
251 #[test]
252 fn driver_should_tick_only_when_running() {
253 let mut driver = AutonomousDriver::new(Duration::from_millis(100));
254 assert!(!driver.should_tick(), "no session → no tick");
255
256 driver.start_session("id", "text", 5);
257 assert!(driver.should_tick(), "Running session → tick");
258
259 if let Some(ref mut s) = driver.session {
260 s.state = AutonomousState::Verifying;
261 }
262 assert!(!driver.should_tick(), "Verifying session → no tick");
263
264 if let Some(ref mut s) = driver.session {
265 s.state = AutonomousState::Achieved;
266 }
267 assert!(!driver.should_tick(), "Achieved session → no tick");
268 }
269
270 #[test]
271 fn start_session_cancels_previous() {
272 let mut driver = AutonomousDriver::new(Duration::from_millis(100));
273 driver.start_session("id1", "first", 10);
274 let prev_cancel = driver.session.as_ref().unwrap().cancel.clone();
275 assert!(!prev_cancel.is_cancelled());
276
277 let cancelled = driver.start_session("id2", "second", 5);
278 assert_eq!(cancelled.as_deref(), Some("id1"));
279 assert!(
280 prev_cancel.is_cancelled(),
281 "previous token must be cancelled"
282 );
283 assert_eq!(driver.session.as_ref().unwrap().goal_id, "id2");
284 }
285
286 #[test]
287 fn abort_clears_session_and_returns_id() {
288 let mut driver = AutonomousDriver::new(Duration::from_millis(100));
289 assert_eq!(driver.abort(), None);
290
291 driver.start_session("id3", "text", 5);
292 let id = driver.abort();
293 assert_eq!(id.as_deref(), Some("id3"));
294 assert!(driver.session.is_none());
295 }
296
297 #[test]
298 fn stuck_detection_logic() {
299 let mut s = AutonomousSession::new("id", "text", 10);
300 assert_eq!(s.stuck_count, 0);
301
302 s.stuck_count = 2;
304 assert!(!s.is_terminal());
305
306 s.stuck_count = 3;
308 s.state = AutonomousState::Stuck;
309 assert!(s.is_terminal());
310 }
311
312 #[test]
313 fn supervisor_fail_count_resets_on_success() {
314 let mut s = AutonomousSession::new("id", "text", 10);
315 s.supervisor_fail_count = 2;
316 s.supervisor_fail_count = 0;
318 assert_eq!(s.supervisor_fail_count, 0);
319 }
320
321 #[test]
322 fn display_covers_all_variants() {
323 assert_eq!(AutonomousState::Running.to_string(), "running");
324 assert_eq!(AutonomousState::Verifying.to_string(), "verifying");
325 assert_eq!(AutonomousState::Achieved.to_string(), "achieved");
326 assert_eq!(AutonomousState::Stuck.to_string(), "stuck");
327 assert_eq!(AutonomousState::Aborted.to_string(), "aborted");
328 assert_eq!(AutonomousState::Failed.to_string(), "failed");
329 }
330
331 #[test]
332 fn driver_new_panics_on_zero_delay() {
333 let result = std::panic::catch_unwind(|| {
334 let _ = AutonomousDriver::new(Duration::ZERO);
335 });
336 assert!(result.is_err(), "zero delay must panic");
337 }
338
339 #[test]
340 fn no_cancelled_id_on_first_start() {
341 let mut driver = AutonomousDriver::new(Duration::from_millis(100));
342 let prev = driver.start_session("id1", "text", 5);
343 assert!(prev.is_none(), "no prior session → no cancelled id");
344 }
345}