use crate::executor::{AgentChild, ChildProcessInfo, ProcessExecutor};
use crate::pipeline::idle_timeout::{
SharedActivityTimestamp, SharedFileActivityTracker, IDLE_TIMEOUT_SECS,
};
use crate::workspace::Workspace;
use std::sync::Arc;
use std::time::Duration;
pub struct FileActivityConfig {
pub tracker: SharedFileActivityTracker,
pub workspace: Arc<dyn Workspace>,
}
#[derive(Clone)]
pub struct MonitorConfig {
pub timeout: Duration,
pub check_interval: Duration,
pub kill_config: crate::pipeline::idle_timeout::io::KillConfig,
pub required_idle_confirmations: u32,
pub check_child_processes: bool,
pub completion_check: Option<Arc<dyn Fn() -> bool + Send + Sync>>,
pub partial_completion_check: Option<Arc<dyn Fn() -> bool + Send + Sync>>,
pub tool_activity_check: Option<Arc<dyn Fn() -> bool + Send + Sync>>,
pub max_tool_suppression_ticks: u32,
}
impl Default for MonitorConfig {
fn default() -> Self {
Self {
timeout: Duration::from_secs(IDLE_TIMEOUT_SECS),
check_interval: DEFAULT_CHECK_INTERVAL,
kill_config: crate::pipeline::idle_timeout::io::DEFAULT_KILL_CONFIG,
required_idle_confirmations: 2,
check_child_processes: true,
completion_check: None,
partial_completion_check: None,
tool_activity_check: None,
max_tool_suppression_ticks: 20,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MonitorResult {
ProcessCompleted,
TimedOut {
escalated: bool,
child_status_at_timeout: Option<ChildProcessInfo>,
},
CompleteButWaiting,
}
pub const DEFAULT_CHECK_INTERVAL: Duration = Duration::from_secs(30);
#[derive(Debug, Clone, Copy)]
pub struct TimeoutEnforcementState {
pub pid: u32,
pub escalated: bool,
pub last_sigkill_sent_at: Option<std::time::Instant>,
pub triggered_at: std::time::Instant,
}
impl TimeoutEnforcementState {
pub fn new(pid: u32, escalated: bool) -> Self {
Self {
pid,
escalated,
triggered_at: std::time::Instant::now(),
last_sigkill_sent_at: escalated.then_some(std::time::Instant::now()),
}
}
}
pub struct MonitorParams<'a> {
pub activity_timestamp: &'a SharedActivityTimestamp,
pub file_activity_config: Option<&'a FileActivityConfig>,
pub child: &'a Arc<std::sync::Mutex<Box<dyn AgentChild>>>,
pub should_stop: &'a Arc<std::sync::atomic::AtomicBool>,
pub executor: &'a Arc<dyn ProcessExecutor>,
pub child_activity_suppressed: Option<&'a Arc<std::sync::Mutex<Option<ChildProcessInfo>>>>,
pub timeout: Duration,
pub check_interval: Duration,
pub kill_config: crate::pipeline::idle_timeout::io::KillConfig,
pub required_idle_confirmations: u32,
pub check_child_processes: bool,
pub completion_check: Option<Arc<dyn Fn() -> bool + Send + Sync>>,
pub partial_completion_check: Option<Arc<dyn Fn() -> bool + Send + Sync>>,
pub tool_activity_check: Option<Arc<dyn Fn() -> bool + Send + Sync>>,
pub max_tool_suppression_ticks: u32,
}
pub(super) enum EnforcementStep {
ReturnResult(MonitorResult),
Continue,
}
pub(super) enum KillResultContinuation {
TimedOut { escalated: bool },
AwaitingExit(TimeoutEnforcementState),
ProcessCompleted,
Continue,
}
#[derive(Debug)]
pub(crate) enum MonitorLoopAction {
Return(MonitorResult),
Continue,
}
pub(super) enum IdleConfirmedAction {
Continue,
Return(MonitorLoopAction),
KillAndReturn(u32),
CompleteAndKill(u32),
}
pub(crate) struct MonitorLoopState {
pub(crate) timeout_triggered: Option<TimeoutEnforcementState>,
pub(crate) last_file_activity: Option<std::time::Instant>,
pub(crate) consecutive_idle_count: u32,
pub(crate) last_child_observation: Option<ChildProcessInfo>,
pub(crate) last_child_info: Option<ChildProcessInfo>,
pub(crate) child_startup_grace_available: bool,
pub(crate) consecutive_tool_suppression_ticks: u32,
pub(crate) tool_suppression_cap_warned: bool,
}
impl MonitorLoopState {
pub(crate) fn new() -> Self {
Self {
timeout_triggered: None,
last_file_activity: None,
consecutive_idle_count: 0,
last_child_observation: None,
last_child_info: None,
child_startup_grace_available: true,
consecutive_tool_suppression_ticks: 0,
tool_suppression_cap_warned: false,
}
}
pub(crate) fn reset_idle(&mut self) {
self.consecutive_idle_count = 0;
self.last_child_observation = None;
self.last_child_info = None;
self.child_startup_grace_available = true;
self.consecutive_tool_suppression_ticks = 0;
self.tool_suppression_cap_warned = false;
}
pub(crate) fn reset_idle_preserving_tool_suppression(&mut self) {
self.consecutive_idle_count = 0;
self.last_child_observation = None;
self.last_child_info = None;
self.child_startup_grace_available = true;
}
}
pub(crate) enum ToolSuppressionAction {
Inactive,
CapExceeded { ticks: u32 },
Suppress { ticks: u32 },
}
#[must_use]
pub(crate) fn evaluate_tool_suppression(
check_result: bool,
current_ticks: u32,
max_ticks: u32,
) -> ToolSuppressionAction {
if !check_result {
return ToolSuppressionAction::Inactive;
}
let ticks = current_ticks.saturating_add(1);
if ticks > max_ticks {
ToolSuppressionAction::CapExceeded { ticks }
} else {
ToolSuppressionAction::Suppress { ticks }
}
}
pub(crate) struct ToolSuppressionEffect {
pub(crate) ticks: u32,
pub(crate) cap_warned: bool,
pub(crate) reset_idle: bool,
pub(crate) diagnostic: Option<String>,
pub(crate) suppressed: bool,
}
#[must_use]
pub(crate) fn resolve_tool_suppression(
action: ToolSuppressionAction,
max_ticks: u32,
already_warned: bool,
) -> ToolSuppressionEffect {
match action {
ToolSuppressionAction::Inactive => ToolSuppressionEffect {
ticks: 0,
cap_warned: false,
reset_idle: false,
diagnostic: None,
suppressed: false,
},
ToolSuppressionAction::CapExceeded { ticks } => ToolSuppressionEffect {
ticks,
cap_warned: true,
reset_idle: false,
diagnostic: (!already_warned).then(|| {
format!(
"Warning: tool-activity suppressor has been active for {ticks} consecutive \
ticks (max {max_ticks}); bypassing suppressor to allow idle-timeout enforcement"
)
}),
suppressed: false,
},
ToolSuppressionAction::Suppress { ticks } => ToolSuppressionEffect {
ticks,
cap_warned: false,
reset_idle: true,
diagnostic: Some(
"Active tool execution detected during idle timeout; \
agent is actively running a tool — continuing monitoring"
.to_owned(),
),
suppressed: true,
},
}
}