vtcode_config/
timeouts.rs1use anyhow::{Result, ensure};
2use serde::{Deserialize, Serialize};
3
4#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
5#[derive(Debug, Clone, Deserialize, Serialize)]
6pub struct TimeoutsConfig {
7 #[serde(default = "TimeoutsConfig::default_default_ceiling_seconds")]
9 pub default_ceiling_seconds: u64,
10 #[serde(default = "TimeoutsConfig::default_pty_ceiling_seconds")]
12 pub pty_ceiling_seconds: u64,
13 #[serde(default = "TimeoutsConfig::default_mcp_ceiling_seconds")]
15 pub mcp_ceiling_seconds: u64,
16 #[serde(default = "TimeoutsConfig::default_streaming_ceiling_seconds")]
18 pub streaming_ceiling_seconds: u64,
19 #[serde(default = "TimeoutsConfig::default_long_running_command_ceiling_seconds")]
21 pub long_running_command_ceiling_seconds: u64,
22 #[serde(default = "TimeoutsConfig::default_warning_threshold_percent")]
24 pub warning_threshold_percent: u8,
25 #[serde(default = "TimeoutsConfig::default_decay_ratio")]
27 pub adaptive_decay_ratio: f64,
28 #[serde(default = "TimeoutsConfig::default_success_streak")]
30 pub adaptive_success_streak: u32,
31 #[serde(default = "TimeoutsConfig::default_min_floor_ms")]
33 pub adaptive_min_floor_ms: u64,
34}
35
36impl Default for TimeoutsConfig {
37 fn default() -> Self {
38 Self {
39 default_ceiling_seconds: Self::default_default_ceiling_seconds(),
40 pty_ceiling_seconds: Self::default_pty_ceiling_seconds(),
41 mcp_ceiling_seconds: Self::default_mcp_ceiling_seconds(),
42 streaming_ceiling_seconds: Self::default_streaming_ceiling_seconds(),
43 long_running_command_ceiling_seconds: Self::default_long_running_command_ceiling_seconds(),
44 warning_threshold_percent: Self::default_warning_threshold_percent(),
45 adaptive_decay_ratio: Self::default_decay_ratio(),
46 adaptive_success_streak: Self::default_success_streak(),
47 adaptive_min_floor_ms: Self::default_min_floor_ms(),
48 }
49 }
50}
51
52impl TimeoutsConfig {
53 const MIN_CEILING_SECONDS: u64 = 15;
54
55 const fn default_default_ceiling_seconds() -> u64 {
56 180
57 }
58
59 const fn default_pty_ceiling_seconds() -> u64 {
60 300
61 }
62
63 const fn default_mcp_ceiling_seconds() -> u64 {
64 120
65 }
66
67 const fn default_streaming_ceiling_seconds() -> u64 {
68 600
69 }
70
71 const fn default_long_running_command_ceiling_seconds() -> u64 {
72 3_600
73 }
74
75 const fn default_warning_threshold_percent() -> u8 {
76 80
77 }
78
79 const fn default_decay_ratio() -> f64 {
80 0.875
81 }
82
83 const fn default_success_streak() -> u32 {
84 5
85 }
86
87 const fn default_min_floor_ms() -> u64 {
88 1_000
89 }
90
91 pub fn warning_threshold_fraction(&self) -> f32 {
93 f32::from(self.warning_threshold_percent) / 100.0
94 }
95
96 pub fn ceiling_duration(&self, seconds: u64) -> Option<std::time::Duration> {
98 if seconds == 0 {
99 None
100 } else {
101 Some(std::time::Duration::from_secs(seconds))
102 }
103 }
104
105 pub(crate) fn validate(&self) -> Result<()> {
106 ensure!(
107 self.warning_threshold_percent > 0 && self.warning_threshold_percent < 100,
108 "timeouts.warning_threshold_percent must be between 1 and 99",
109 );
110
111 ensure!(
112 (0.1..=1.0).contains(&self.adaptive_decay_ratio),
113 "timeouts.adaptive_decay_ratio must be between 0.1 and 1.0"
114 );
115 ensure!(self.adaptive_success_streak > 0, "timeouts.adaptive_success_streak must be at least 1");
116 ensure!(self.adaptive_min_floor_ms >= 100, "timeouts.adaptive_min_floor_ms must be at least 100ms");
117
118 ensure!(
119 self.default_ceiling_seconds == 0 || self.default_ceiling_seconds >= Self::MIN_CEILING_SECONDS,
120 "timeouts.default_ceiling_seconds must be at least {} seconds (or 0 to disable)",
121 Self::MIN_CEILING_SECONDS
122 );
123
124 ensure!(
125 self.pty_ceiling_seconds == 0 || self.pty_ceiling_seconds >= Self::MIN_CEILING_SECONDS,
126 "timeouts.pty_ceiling_seconds must be at least {} seconds (or 0 to disable)",
127 Self::MIN_CEILING_SECONDS
128 );
129
130 ensure!(
131 self.mcp_ceiling_seconds == 0 || self.mcp_ceiling_seconds >= Self::MIN_CEILING_SECONDS,
132 "timeouts.mcp_ceiling_seconds must be at least {} seconds (or 0 to disable)",
133 Self::MIN_CEILING_SECONDS
134 );
135
136 ensure!(
137 self.streaming_ceiling_seconds == 0 || self.streaming_ceiling_seconds >= Self::MIN_CEILING_SECONDS,
138 "timeouts.streaming_ceiling_seconds must be at least {} seconds (or 0 to disable)",
139 Self::MIN_CEILING_SECONDS
140 );
141
142 ensure!(
143 self.long_running_command_ceiling_seconds == 0
144 || self.long_running_command_ceiling_seconds >= Self::MIN_CEILING_SECONDS,
145 "timeouts.long_running_command_ceiling_seconds must be at least {} seconds (or 0 to disable)",
146 Self::MIN_CEILING_SECONDS
147 );
148
149 Ok(())
150 }
151}
152
153pub fn resolve_timeout(user_timeout: Option<u64>) -> u64 {
155 use crate::constants::execution::{DEFAULT_TIMEOUT_SECS, MAX_TIMEOUT_SECS, MIN_TIMEOUT_SECS};
156
157 match user_timeout {
158 None | Some(0) => DEFAULT_TIMEOUT_SECS,
159 Some(value) if value < MIN_TIMEOUT_SECS => MIN_TIMEOUT_SECS,
160 Some(value) if value > MAX_TIMEOUT_SECS => MAX_TIMEOUT_SECS,
161 Some(value) => value,
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::TimeoutsConfig;
168 use super::resolve_timeout;
169 use crate::constants::execution::{DEFAULT_TIMEOUT_SECS, MAX_TIMEOUT_SECS, MIN_TIMEOUT_SECS};
170
171 #[test]
172 fn default_values_are_safe() {
173 let config = TimeoutsConfig::default();
174 assert_eq!(config.default_ceiling_seconds, 180);
175 assert_eq!(config.pty_ceiling_seconds, 300);
176 assert_eq!(config.mcp_ceiling_seconds, 120);
177 assert_eq!(config.streaming_ceiling_seconds, 600);
178 assert_eq!(config.long_running_command_ceiling_seconds, 3_600);
179 assert_eq!(config.warning_threshold_percent, 80);
180 assert!(config.validate().is_ok());
181 }
182
183 #[test]
184 fn zero_ceiling_disables_limit() {
185 let config = TimeoutsConfig { default_ceiling_seconds: 0, ..Default::default() };
186 assert!(config.validate().is_ok());
187 assert!(config.ceiling_duration(config.default_ceiling_seconds).is_none());
188 }
189
190 #[test]
191 fn warning_threshold_bounds_are_enforced() {
192 let config_low = TimeoutsConfig { warning_threshold_percent: 0, ..Default::default() };
193 assert!(config_low.validate().is_err());
194
195 let config_high = TimeoutsConfig {
196 warning_threshold_percent: 100,
197 ..Default::default()
198 };
199 assert!(config_high.validate().is_err());
200 }
201
202 #[test]
203 fn resolve_timeout_applies_bounds() {
204 assert_eq!(resolve_timeout(None), DEFAULT_TIMEOUT_SECS);
205 assert_eq!(resolve_timeout(Some(0)), DEFAULT_TIMEOUT_SECS);
206 assert_eq!(resolve_timeout(Some(1)), MIN_TIMEOUT_SECS);
207 assert_eq!(resolve_timeout(Some(MAX_TIMEOUT_SECS + 1)), MAX_TIMEOUT_SECS);
208 assert_eq!(resolve_timeout(Some(120)), 120);
209 }
210}