use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use super::health::{HealthMonitor, HealthMonitorConfig, HealthStatus};
use super::hibernate::AgentLifecycleManager;
use crate::safety::{ApprovalPolicy, AutonomousOperation, SafetyStop};
#[derive(Debug, Clone)]
pub enum SupervisorAction {
Ignore,
Restart,
Shutdown,
Escalate,
CrashRecover,
}
#[derive(Debug, Clone)]
pub struct SupervisorConfig {
pub check_interval_secs: u64,
pub max_restarts: u32,
pub on_degraded: SupervisorAction,
pub on_unhealthy: SupervisorAction,
}
impl Default for SupervisorConfig {
fn default() -> Self {
Self {
check_interval_secs: 30,
max_restarts: 3,
on_degraded: SupervisorAction::Ignore,
on_unhealthy: SupervisorAction::Restart,
}
}
}
pub struct AgentSupervisor {
config: SupervisorConfig,
health: Arc<RwLock<HealthMonitor>>,
lifecycle: Arc<dyn AgentLifecycleManager>,
approval: Arc<dyn ApprovalPolicy>,
restart_counts: HashMap<String, u32>,
}
impl AgentSupervisor {
pub fn new(
config: SupervisorConfig,
lifecycle: Arc<dyn AgentLifecycleManager>,
approval: Arc<dyn ApprovalPolicy>,
) -> Self {
Self {
health: Arc::new(RwLock::new(HealthMonitor::new(
HealthMonitorConfig::default(),
))),
config,
lifecycle,
approval,
restart_counts: HashMap::new(),
}
}
pub fn health_monitor(&self) -> Arc<RwLock<HealthMonitor>> {
self.health.clone()
}
pub async fn check_cycle(&mut self) -> Vec<SupervisorEvent> {
let mut events = Vec::new();
let degraded = self.health.write().await.evaluate_all();
for (agent_id, status, signals) in degraded {
let action = match status {
HealthStatus::Degraded => &self.config.on_degraded,
HealthStatus::Unhealthy | HealthStatus::Unknown => &self.config.on_unhealthy,
HealthStatus::Healthy => continue,
};
match action {
SupervisorAction::Ignore => {
tracing::debug!("Supervisor: ignoring degraded agent {agent_id}");
}
SupervisorAction::Restart => {
let count = self.restart_counts.entry(agent_id.clone()).or_insert(0);
if *count >= self.config.max_restarts {
tracing::warn!(
"Supervisor: agent {agent_id} exceeded max restarts ({}), escalating",
self.config.max_restarts
);
events.push(SupervisorEvent::Escalated {
agent_id: agent_id.clone(),
reason: "max restarts exceeded".to_string(),
});
continue;
}
let op = AutonomousOperation::RestartAgent {
agent_id: agent_id.clone(),
reason: format!("health status: {status:?}, signals: {signals:?}"),
};
match self.approval.check(&op).await {
Ok(()) => match self.lifecycle.shutdown(&agent_id).await {
Ok(()) => {
*count += 1;
tracing::info!(
"Supervisor: restarted agent {agent_id} (attempt {count})"
);
events.push(SupervisorEvent::Restarted {
agent_id: agent_id.clone(),
attempt: *count,
});
}
Err(e) => {
tracing::error!("Supervisor: failed to restart {agent_id}: {e}");
events.push(SupervisorEvent::RestartFailed {
agent_id: agent_id.clone(),
error: e.to_string(),
});
}
},
Err(SafetyStop::OperationRejected(reason)) => {
tracing::warn!("Supervisor: restart of {agent_id} rejected: {reason}");
events.push(SupervisorEvent::Escalated {
agent_id: agent_id.clone(),
reason,
});
}
Err(stop) => {
tracing::warn!("Supervisor: safety stop for {agent_id}: {stop}");
}
}
}
SupervisorAction::Shutdown => {
if let Err(e) = self.lifecycle.shutdown(&agent_id).await {
tracing::error!("Supervisor: failed to shut down {agent_id}: {e}");
} else {
events.push(SupervisorEvent::ShutDown {
agent_id: agent_id.clone(),
});
}
}
SupervisorAction::CrashRecover => {
tracing::info!("Supervisor: initiating crash recovery for agent {agent_id}");
events.push(SupervisorEvent::CrashRecoveryStarted {
agent_id: agent_id.clone(),
crash_id: uuid::Uuid::new_v4().to_string(),
});
}
SupervisorAction::Escalate => {
events.push(SupervisorEvent::Escalated {
agent_id: agent_id.clone(),
reason: format!("status: {status:?}"),
});
}
}
}
events
}
pub async fn run(&mut self, cancel: tokio::sync::watch::Receiver<bool>) {
let interval = std::time::Duration::from_secs(self.config.check_interval_secs);
loop {
if *cancel.borrow() {
break;
}
let events = self.check_cycle().await;
for event in &events {
tracing::info!("Supervisor event: {event:?}");
}
tokio::time::sleep(interval).await;
}
}
}
#[derive(Debug, Clone)]
pub enum SupervisorEvent {
Restarted {
agent_id: String,
attempt: u32,
},
RestartFailed {
agent_id: String,
error: String,
},
ShutDown {
agent_id: String,
},
Escalated {
agent_id: String,
reason: String,
},
CrashRecoveryStarted {
agent_id: String,
crash_id: String,
},
CrashRecovered {
agent_id: String,
fix_summary: String,
},
CrashRecoveryFailed {
agent_id: String,
error: String,
},
}