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}
22
23impl ToolTimeoutCategory {
24 pub fn label(&self) -> &'static str {
26 match self {
27 ToolTimeoutCategory::Default => "standard",
28 ToolTimeoutCategory::Pty => "PTY",
29 ToolTimeoutCategory::Mcp => "MCP",
30 }
31 }
32}
33
34#[derive(Debug, Clone)]
36pub struct ToolTimeoutPolicy {
37 default_ceiling: Option<Duration>,
38 pty_ceiling: Option<Duration>,
39 mcp_ceiling: Option<Duration>,
40 warning_fraction: f32,
41}
42
43impl Default for ToolTimeoutPolicy {
44 fn default() -> Self {
45 Self {
46 default_ceiling: Some(Duration::from_secs(180)),
47 pty_ceiling: Some(Duration::from_secs(300)),
48 mcp_ceiling: Some(Duration::from_secs(120)),
49 warning_fraction: 0.75,
50 }
51 }
52}
53
54impl ToolTimeoutPolicy {
55 pub fn from_config(config: &TimeoutsConfig) -> Self {
57 Self {
58 default_ceiling: config.ceiling_duration(config.default_ceiling_seconds),
59 pty_ceiling: config.ceiling_duration(config.pty_ceiling_seconds),
60 mcp_ceiling: config.ceiling_duration(config.mcp_ceiling_seconds),
61 warning_fraction: config.warning_threshold_fraction().clamp(0.0, 0.99),
62 }
63 }
64
65 #[inline]
67 fn validate_ceiling(ceiling: Option<Duration>, name: &str) -> anyhow::Result<()> {
68 if let Some(ceiling) = ceiling {
69 if ceiling < Duration::from_secs(1) {
70 anyhow::bail!(
71 "{} must be at least 1 second (got {}s)",
72 name,
73 ceiling.as_secs()
74 );
75 }
76 if ceiling > Duration::from_secs(3600) {
77 anyhow::bail!(
78 "{} must not exceed 3600 seconds/1 hour (got {}s)",
79 name,
80 ceiling.as_secs()
81 );
82 }
83 }
84 Ok(())
85 }
86
87 pub fn validate(&self) -> anyhow::Result<()> {
94 Self::validate_ceiling(self.default_ceiling, "default_ceiling_seconds")?;
95 Self::validate_ceiling(self.pty_ceiling, "pty_ceiling_seconds")?;
96 Self::validate_ceiling(self.mcp_ceiling, "mcp_ceiling_seconds")?;
97
98 if self.warning_fraction <= 0.0 {
100 anyhow::bail!(
101 "warning_threshold_percent must be greater than 0 (got {})",
102 self.warning_fraction * 100.0
103 );
104 }
105 if self.warning_fraction >= 1.0 {
106 anyhow::bail!(
107 "warning_threshold_percent must be less than 100 (got {})",
108 self.warning_fraction * 100.0
109 );
110 }
111
112 Ok(())
113 }
114
115 pub fn ceiling_for(&self, category: ToolTimeoutCategory) -> Option<Duration> {
117 match category {
118 ToolTimeoutCategory::Default => self.default_ceiling,
119 ToolTimeoutCategory::Pty => self.pty_ceiling.or(self.default_ceiling),
120 ToolTimeoutCategory::Mcp => self.mcp_ceiling.or(self.default_ceiling),
121 }
122 }
123
124 pub fn warning_fraction(&self) -> f32 {
126 self.warning_fraction
127 }
128}
129
130#[derive(Debug, Clone, Default)]
132pub struct ToolLatencyStats {
133 pub(super) samples: VecDeque<Duration>,
134 pub(super) max_samples: usize,
135}
136
137impl ToolLatencyStats {
138 pub fn new(max_samples: usize) -> Self {
140 Self {
141 samples: VecDeque::with_capacity(max_samples),
142 max_samples,
143 }
144 }
145
146 pub fn record(&mut self, duration: Duration) {
148 if self.samples.len() >= self.max_samples {
149 self.samples.pop_front();
150 }
151 self.samples.push_back(duration);
152 }
153
154 pub fn percentile(&self, pct: f64) -> Option<Duration> {
156 if self.samples.is_empty() {
157 return None;
158 }
159 let mut sorted: Vec<Duration> = self.samples.iter().copied().collect();
160 sorted.sort_unstable();
161 let idx =
162 ((pct.clamp(0.0, 1.0)) * (sorted.len().saturating_sub(1) as f64)).round() as usize;
163 sorted.get(idx).copied()
164 }
165}
166
167#[derive(Debug, Clone, Copy)]
169pub struct AdaptiveTimeoutTuning {
170 pub decay_ratio: f64,
172 pub success_streak: u32,
174 pub min_floor_ms: u64,
176}
177
178impl Default for AdaptiveTimeoutTuning {
179 fn default() -> Self {
180 Self {
181 decay_ratio: 0.875, success_streak: 5, min_floor_ms: 1_000, }
185 }
186}
187
188impl AdaptiveTimeoutTuning {
189 pub fn from_config(timeouts: &TimeoutsConfig) -> Self {
191 Self {
192 decay_ratio: timeouts.adaptive_decay_ratio,
193 success_streak: timeouts.adaptive_success_streak,
194 min_floor_ms: timeouts.adaptive_min_floor_ms,
195 }
196 }
197}