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