use std::collections::VecDeque;
use std::time::Duration;
use crate::config::TimeoutsConfig;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ToolTimeoutCategory {
Default,
Pty,
Mcp,
}
impl ToolTimeoutCategory {
pub fn label(&self) -> &'static str {
match self {
ToolTimeoutCategory::Default => "standard",
ToolTimeoutCategory::Pty => "PTY",
ToolTimeoutCategory::Mcp => "MCP",
}
}
}
#[derive(Debug, Clone)]
pub struct ToolTimeoutPolicy {
default_ceiling: Option<Duration>,
pty_ceiling: Option<Duration>,
mcp_ceiling: Option<Duration>,
warning_fraction: f32,
}
impl Default for ToolTimeoutPolicy {
fn default() -> Self {
Self {
default_ceiling: Some(Duration::from_secs(180)),
pty_ceiling: Some(Duration::from_secs(300)),
mcp_ceiling: Some(Duration::from_secs(120)),
warning_fraction: 0.75,
}
}
}
impl ToolTimeoutPolicy {
pub fn from_config(config: &TimeoutsConfig) -> Self {
Self {
default_ceiling: config.ceiling_duration(config.default_ceiling_seconds),
pty_ceiling: config.ceiling_duration(config.pty_ceiling_seconds),
mcp_ceiling: config.ceiling_duration(config.mcp_ceiling_seconds),
warning_fraction: config.warning_threshold_fraction().clamp(0.0, 0.99),
}
}
#[inline]
fn validate_ceiling(ceiling: Option<Duration>, name: &str) -> anyhow::Result<()> {
if let Some(ceiling) = ceiling {
if ceiling < Duration::from_secs(1) {
anyhow::bail!(
"{} must be at least 1 second (got {}s)",
name,
ceiling.as_secs()
);
}
if ceiling > Duration::from_secs(3600) {
anyhow::bail!(
"{} must not exceed 3600 seconds/1 hour (got {}s)",
name,
ceiling.as_secs()
);
}
}
Ok(())
}
pub fn validate(&self) -> anyhow::Result<()> {
Self::validate_ceiling(self.default_ceiling, "default_ceiling_seconds")?;
Self::validate_ceiling(self.pty_ceiling, "pty_ceiling_seconds")?;
Self::validate_ceiling(self.mcp_ceiling, "mcp_ceiling_seconds")?;
if self.warning_fraction <= 0.0 {
anyhow::bail!(
"warning_threshold_percent must be greater than 0 (got {})",
self.warning_fraction * 100.0
);
}
if self.warning_fraction >= 1.0 {
anyhow::bail!(
"warning_threshold_percent must be less than 100 (got {})",
self.warning_fraction * 100.0
);
}
Ok(())
}
pub fn ceiling_for(&self, category: ToolTimeoutCategory) -> Option<Duration> {
match category {
ToolTimeoutCategory::Default => self.default_ceiling,
ToolTimeoutCategory::Pty => self.pty_ceiling.or(self.default_ceiling),
ToolTimeoutCategory::Mcp => self.mcp_ceiling.or(self.default_ceiling),
}
}
pub fn warning_fraction(&self) -> f32 {
self.warning_fraction
}
}
#[derive(Debug, Clone, Default)]
pub struct ToolLatencyStats {
pub(super) samples: VecDeque<Duration>,
pub(super) max_samples: usize,
}
impl ToolLatencyStats {
pub fn new(max_samples: usize) -> Self {
Self {
samples: VecDeque::with_capacity(max_samples),
max_samples,
}
}
pub fn record(&mut self, duration: Duration) {
if self.samples.len() >= self.max_samples {
self.samples.pop_front();
}
self.samples.push_back(duration);
}
pub fn percentile(&self, pct: f64) -> Option<Duration> {
if self.samples.is_empty() {
return None;
}
let mut sorted: Vec<Duration> = self.samples.iter().copied().collect();
sorted.sort_unstable();
let idx =
((pct.clamp(0.0, 1.0)) * (sorted.len().saturating_sub(1) as f64)).round() as usize;
sorted.get(idx).copied()
}
}
#[derive(Debug, Clone, Copy)]
pub struct AdaptiveTimeoutTuning {
pub decay_ratio: f64,
pub success_streak: u32,
pub min_floor_ms: u64,
}
impl Default for AdaptiveTimeoutTuning {
fn default() -> Self {
Self {
decay_ratio: 0.875, success_streak: 5, min_floor_ms: 1_000, }
}
}
impl AdaptiveTimeoutTuning {
pub fn from_config(timeouts: &TimeoutsConfig) -> Self {
Self {
decay_ratio: timeouts.adaptive_decay_ratio,
success_streak: timeouts.adaptive_success_streak,
min_floor_ms: timeouts.adaptive_min_floor_ms,
}
}
}