vtcode_core/tools/registry/
timeout.rs1use std::collections::VecDeque;
8use std::time::Duration;
9
10use crate::config::TimeoutsConfig;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum ToolTimeoutCategory {
15 Default,
17 Pty,
19 Mcp,
21 LongRunningCommand,
23}
24
25impl ToolTimeoutCategory {
26 pub fn label(&self) -> &'static str {
28 match self {
29 ToolTimeoutCategory::Default => "standard",
30 ToolTimeoutCategory::Pty => "PTY",
31 ToolTimeoutCategory::Mcp => "MCP",
32 ToolTimeoutCategory::LongRunningCommand => "long-running command",
33 }
34 }
35}
36
37#[derive(Debug, Clone)]
39pub struct ToolTimeoutPolicy {
40 default_ceiling: Option<Duration>,
41 pty_ceiling: Option<Duration>,
42 mcp_ceiling: Option<Duration>,
43 long_running_command_ceiling: Option<Duration>,
44 warning_fraction: f32,
45}
46
47impl Default for ToolTimeoutPolicy {
48 fn default() -> Self {
49 Self {
50 default_ceiling: Some(Duration::from_secs(180)),
51 pty_ceiling: Some(Duration::from_secs(300)),
52 mcp_ceiling: Some(Duration::from_secs(120)),
53 long_running_command_ceiling: Some(Duration::from_secs(3_600)),
54 warning_fraction: 0.75,
55 }
56 }
57}
58
59impl ToolTimeoutPolicy {
60 pub fn from_config(config: &TimeoutsConfig) -> Self {
62 Self {
63 default_ceiling: config.ceiling_duration(config.default_ceiling_seconds),
64 pty_ceiling: config.ceiling_duration(config.pty_ceiling_seconds),
65 mcp_ceiling: config.ceiling_duration(config.mcp_ceiling_seconds),
66 long_running_command_ceiling: config.ceiling_duration(config.long_running_command_ceiling_seconds),
67 warning_fraction: config.warning_threshold_fraction().clamp(0.0, 0.99),
68 }
69 }
70
71 #[inline]
73 fn validate_ceiling(ceiling: Option<Duration>, name: &str) -> anyhow::Result<()> {
74 if let Some(ceiling) = ceiling {
75 if ceiling < Duration::from_secs(1) {
76 anyhow::bail!("{} must be at least 1 second (got {}s)", name, ceiling.as_secs());
77 }
78 if ceiling > Duration::from_secs(3600) {
79 anyhow::bail!("{} must not exceed 3600 seconds/1 hour (got {}s)", name, ceiling.as_secs());
80 }
81 }
82 Ok(())
83 }
84
85 pub fn validate(&self) -> anyhow::Result<()> {
92 Self::validate_ceiling(self.default_ceiling, "default_ceiling_seconds")?;
93 Self::validate_ceiling(self.pty_ceiling, "pty_ceiling_seconds")?;
94 Self::validate_ceiling(self.mcp_ceiling, "mcp_ceiling_seconds")?;
95 Self::validate_ceiling(self.long_running_command_ceiling, "long_running_command_ceiling_seconds")?;
96
97 if self.warning_fraction <= 0.0 {
99 anyhow::bail!("warning_threshold_percent must be greater than 0 (got {})", self.warning_fraction * 100.0);
100 }
101 if self.warning_fraction >= 1.0 {
102 anyhow::bail!("warning_threshold_percent must be less than 100 (got {})", self.warning_fraction * 100.0);
103 }
104
105 Ok(())
106 }
107
108 pub fn ceiling_for(&self, category: ToolTimeoutCategory) -> Option<Duration> {
110 match category {
111 ToolTimeoutCategory::Default => self.default_ceiling,
112 ToolTimeoutCategory::Pty => self.pty_ceiling.or(self.default_ceiling),
113 ToolTimeoutCategory::Mcp => self.mcp_ceiling.or(self.default_ceiling),
114 ToolTimeoutCategory::LongRunningCommand => self.long_running_command_ceiling.or(self.default_ceiling),
115 }
116 }
117
118 pub fn warning_fraction(&self) -> f32 {
120 self.warning_fraction
121 }
122}
123
124#[derive(Debug, Clone, Default)]
126pub struct ToolLatencyStats {
127 samples: VecDeque<Duration>,
128 max_samples: usize,
129}
130
131impl ToolLatencyStats {
132 pub fn new(max_samples: usize) -> Self {
134 Self {
135 samples: VecDeque::with_capacity(max_samples),
136 max_samples,
137 }
138 }
139
140 pub fn record(&mut self, duration: Duration) {
142 if self.samples.len() >= self.max_samples {
143 self.samples.pop_front();
144 }
145 self.samples.push_back(duration);
146 }
147
148 pub fn percentile(&self, pct: f64) -> Option<Duration> {
150 if self.samples.is_empty() {
151 return None;
152 }
153 let mut sorted: Vec<Duration> = self.samples.iter().copied().collect();
154 sorted.sort_unstable();
155 #[allow(
156 clippy::cast_sign_loss,
157 reason = "Intentional compatibility, platform, or test-only suppression."
158 )]
159 let idx = (((pct.clamp(0.0, 1.0)) * (sorted.len().saturating_sub(1) as f64)).round()).max(0.0) as usize;
160 sorted.get(idx).copied()
161 }
162}
163
164#[derive(Debug, Clone, Copy)]
166pub struct AdaptiveTimeoutTuning {
167 pub decay_ratio: f64,
169 pub success_streak: u32,
171 pub min_floor_ms: u64,
173}
174
175impl Default for AdaptiveTimeoutTuning {
176 fn default() -> Self {
177 Self {
178 decay_ratio: 0.875, success_streak: 5, min_floor_ms: 1_000, }
182 }
183}
184
185impl AdaptiveTimeoutTuning {
186 pub fn from_config(timeouts: &TimeoutsConfig) -> Self {
188 Self {
189 decay_ratio: timeouts.adaptive_decay_ratio,
190 success_streak: timeouts.adaptive_success_streak,
191 min_floor_ms: timeouts.adaptive_min_floor_ms,
192 }
193 }
194}