use std::sync::Arc;
use std::time::Instant;
use parking_lot::Mutex;
use tokio::time::{Duration, Instant as TokioInstant, Interval};
use tokio_util::sync::CancellationToken;
pub use zeph_config::autonomous::AutonomousState;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SupervisorVerdict {
pub achieved: bool,
pub reasoning: String,
pub confidence: f32,
pub suggestions: Vec<String>,
}
pub struct AutonomousSession {
pub goal_id: String,
pub goal_text: String,
pub state: AutonomousState,
pub turns_executed: u32,
pub max_turns: u32,
pub stuck_count: u32,
pub supervisor_fail_count: u32,
pub cancel: CancellationToken,
pub last_verdict: Option<SupervisorVerdict>,
pub started_at: Instant,
pub supervisor_retry_at: Option<TokioInstant>,
}
impl AutonomousSession {
#[must_use]
pub fn new(goal_id: impl Into<String>, goal_text: impl Into<String>, max_turns: u32) -> Self {
Self {
goal_id: goal_id.into(),
goal_text: goal_text.into(),
state: AutonomousState::Running,
turns_executed: 0,
max_turns,
stuck_count: 0,
supervisor_fail_count: 0,
cancel: CancellationToken::new(),
last_verdict: None,
started_at: Instant::now(),
supervisor_retry_at: None,
}
}
#[must_use]
pub fn is_terminal(&self) -> bool {
matches!(
self.state,
AutonomousState::Achieved
| AutonomousState::Stuck
| AutonomousState::Aborted
| AutonomousState::Failed
)
}
#[must_use]
pub fn elapsed(&self) -> Duration {
self.started_at.elapsed()
}
}
pub struct AutonomousDriver {
pub session: Option<AutonomousSession>,
turn_delay: Duration,
pub(crate) turn_interval: Option<Interval>,
pub pending_start_arc: Arc<Mutex<Option<(String, String, u32)>>>,
}
impl AutonomousDriver {
#[must_use]
pub fn new(turn_delay: Duration) -> Self {
assert!(
!turn_delay.is_zero(),
"autonomous turn delay must be non-zero"
);
Self {
session: None,
turn_delay,
turn_interval: None,
pending_start_arc: Arc::new(Mutex::new(None)),
}
}
#[must_use]
pub fn flush_pending_start(&mut self) -> Option<(Option<String>, String)> {
let pending = self.pending_start_arc.lock().take()?;
let (goal_id, goal_text, max_turns) = pending;
let new_id = goal_id.clone();
let cancelled = self.start_session(goal_id, goal_text, max_turns);
Some((cancelled, new_id))
}
#[must_use]
pub fn should_tick(&self) -> bool {
self.session
.as_ref()
.is_some_and(|s| s.state == AutonomousState::Running)
}
pub async fn next_tick(&mut self) {
let interval = self.turn_interval.get_or_insert_with(|| {
let mut iv = tokio::time::interval(self.turn_delay);
iv.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
iv
});
interval.tick().await;
}
pub fn start_session(
&mut self,
goal_id: impl Into<String>,
goal_text: impl Into<String>,
max_turns: u32,
) -> Option<String> {
let prev = self.session.take();
let cancelled_id = prev.map(|s| {
s.cancel.cancel();
s.goal_id
});
self.session = Some(AutonomousSession::new(goal_id, goal_text, max_turns));
cancelled_id
}
pub fn abort(&mut self) -> Option<String> {
let s = self.session.as_mut()?;
s.cancel.cancel();
s.state = AutonomousState::Aborted;
let id = s.goal_id.clone();
self.session = None;
Some(id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn initial_state_is_running() {
let s = AutonomousSession::new("id1", "do something", 10);
assert_eq!(s.state, AutonomousState::Running);
assert_eq!(s.turns_executed, 0);
assert!(!s.is_terminal());
}
#[test]
fn terminal_states() {
for terminal in [
AutonomousState::Achieved,
AutonomousState::Stuck,
AutonomousState::Aborted,
AutonomousState::Failed,
] {
let mut s = AutonomousSession::new("id", "text", 5);
s.state = terminal;
assert!(s.is_terminal(), "{terminal} should be terminal");
}
}
#[test]
fn running_and_verifying_are_not_terminal() {
for non_terminal in [AutonomousState::Running, AutonomousState::Verifying] {
let mut s = AutonomousSession::new("id", "text", 5);
s.state = non_terminal;
assert!(!s.is_terminal(), "{non_terminal} should not be terminal");
}
}
#[test]
fn driver_should_tick_only_when_running() {
let mut driver = AutonomousDriver::new(Duration::from_millis(100));
assert!(!driver.should_tick(), "no session → no tick");
driver.start_session("id", "text", 5);
assert!(driver.should_tick(), "Running session → tick");
if let Some(ref mut s) = driver.session {
s.state = AutonomousState::Verifying;
}
assert!(!driver.should_tick(), "Verifying session → no tick");
if let Some(ref mut s) = driver.session {
s.state = AutonomousState::Achieved;
}
assert!(!driver.should_tick(), "Achieved session → no tick");
}
#[test]
fn start_session_cancels_previous() {
let mut driver = AutonomousDriver::new(Duration::from_millis(100));
driver.start_session("id1", "first", 10);
let prev_cancel = driver.session.as_ref().unwrap().cancel.clone();
assert!(!prev_cancel.is_cancelled());
let cancelled = driver.start_session("id2", "second", 5);
assert_eq!(cancelled.as_deref(), Some("id1"));
assert!(
prev_cancel.is_cancelled(),
"previous token must be cancelled"
);
assert_eq!(driver.session.as_ref().unwrap().goal_id, "id2");
}
#[test]
fn abort_clears_session_and_returns_id() {
let mut driver = AutonomousDriver::new(Duration::from_millis(100));
assert_eq!(driver.abort(), None);
driver.start_session("id3", "text", 5);
let id = driver.abort();
assert_eq!(id.as_deref(), Some("id3"));
assert!(driver.session.is_none());
}
#[test]
fn stuck_detection_logic() {
let mut s = AutonomousSession::new("id", "text", 10);
assert_eq!(s.stuck_count, 0);
s.stuck_count = 2;
assert!(!s.is_terminal());
s.stuck_count = 3;
s.state = AutonomousState::Stuck;
assert!(s.is_terminal());
}
#[test]
fn supervisor_fail_count_resets_on_success() {
let mut s = AutonomousSession::new("id", "text", 10);
s.supervisor_fail_count = 2;
s.supervisor_fail_count = 0;
assert_eq!(s.supervisor_fail_count, 0);
}
#[test]
fn display_covers_all_variants() {
assert_eq!(AutonomousState::Running.to_string(), "running");
assert_eq!(AutonomousState::Verifying.to_string(), "verifying");
assert_eq!(AutonomousState::Achieved.to_string(), "achieved");
assert_eq!(AutonomousState::Stuck.to_string(), "stuck");
assert_eq!(AutonomousState::Aborted.to_string(), "aborted");
assert_eq!(AutonomousState::Failed.to_string(), "failed");
}
#[test]
fn driver_new_panics_on_zero_delay() {
let result = std::panic::catch_unwind(|| {
let _ = AutonomousDriver::new(Duration::ZERO);
});
assert!(result.is_err(), "zero delay must panic");
}
#[test]
fn no_cancelled_id_on_first_start() {
let mut driver = AutonomousDriver::new(Duration::from_millis(100));
let prev = driver.start_session("id1", "text", 5);
assert!(prev.is_none(), "no prior session → no cancelled id");
}
}