vtcode_config/
timeouts.rs

1use 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    /// Maximum duration (in seconds) for standard, non-PTY tools.
8    #[serde(default = "TimeoutsConfig::default_default_ceiling_seconds")]
9    pub default_ceiling_seconds: u64,
10    /// Maximum duration (in seconds) for PTY-backed commands.
11    #[serde(default = "TimeoutsConfig::default_pty_ceiling_seconds")]
12    pub pty_ceiling_seconds: u64,
13    /// Maximum duration (in seconds) for MCP calls.
14    #[serde(default = "TimeoutsConfig::default_mcp_ceiling_seconds")]
15    pub mcp_ceiling_seconds: u64,
16    /// Maximum duration (in seconds) for streaming API responses.
17    #[serde(default = "TimeoutsConfig::default_streaming_ceiling_seconds")]
18    pub streaming_ceiling_seconds: u64,
19    /// Percentage (0-100) of the ceiling after which the UI should warn.
20    #[serde(default = "TimeoutsConfig::default_warning_threshold_percent")]
21    pub warning_threshold_percent: u8,
22}
23
24impl Default for TimeoutsConfig {
25    fn default() -> Self {
26        Self {
27            default_ceiling_seconds: Self::default_default_ceiling_seconds(),
28            pty_ceiling_seconds: Self::default_pty_ceiling_seconds(),
29            mcp_ceiling_seconds: Self::default_mcp_ceiling_seconds(),
30            streaming_ceiling_seconds: Self::default_streaming_ceiling_seconds(),
31            warning_threshold_percent: Self::default_warning_threshold_percent(),
32        }
33    }
34}
35
36impl TimeoutsConfig {
37    const MIN_CEILING_SECONDS: u64 = 15;
38
39    const fn default_default_ceiling_seconds() -> u64 {
40        180
41    }
42
43    const fn default_pty_ceiling_seconds() -> u64 {
44        300
45    }
46
47    const fn default_mcp_ceiling_seconds() -> u64 {
48        120
49    }
50
51    const fn default_streaming_ceiling_seconds() -> u64 {
52        600
53    }
54
55    const fn default_warning_threshold_percent() -> u8 {
56        80
57    }
58
59    /// Convert the configured threshold into a fraction (0.0-1.0).
60    pub fn warning_threshold_fraction(&self) -> f32 {
61        f32::from(self.warning_threshold_percent) / 100.0
62    }
63
64    /// Normalize a ceiling value into an optional duration.
65    pub fn ceiling_duration(&self, seconds: u64) -> Option<std::time::Duration> {
66        if seconds == 0 {
67            None
68        } else {
69            Some(std::time::Duration::from_secs(seconds))
70        }
71    }
72
73    pub fn validate(&self) -> Result<()> {
74        ensure!(
75            self.warning_threshold_percent > 0 && self.warning_threshold_percent < 100,
76            "timeouts.warning_threshold_percent must be between 1 and 99",
77        );
78
79        ensure!(
80            self.default_ceiling_seconds == 0
81                || self.default_ceiling_seconds >= Self::MIN_CEILING_SECONDS,
82            "timeouts.default_ceiling_seconds must be at least {} seconds (or 0 to disable)",
83            Self::MIN_CEILING_SECONDS
84        );
85
86        ensure!(
87            self.pty_ceiling_seconds == 0 || self.pty_ceiling_seconds >= Self::MIN_CEILING_SECONDS,
88            "timeouts.pty_ceiling_seconds must be at least {} seconds (or 0 to disable)",
89            Self::MIN_CEILING_SECONDS
90        );
91
92        ensure!(
93            self.mcp_ceiling_seconds == 0 || self.mcp_ceiling_seconds >= Self::MIN_CEILING_SECONDS,
94            "timeouts.mcp_ceiling_seconds must be at least {} seconds (or 0 to disable)",
95            Self::MIN_CEILING_SECONDS
96        );
97
98        ensure!(
99            self.streaming_ceiling_seconds == 0
100                || self.streaming_ceiling_seconds >= Self::MIN_CEILING_SECONDS,
101            "timeouts.streaming_ceiling_seconds must be at least {} seconds (or 0 to disable)",
102            Self::MIN_CEILING_SECONDS
103        );
104
105        Ok(())
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::TimeoutsConfig;
112
113    #[test]
114    fn default_values_are_safe() {
115        let config = TimeoutsConfig::default();
116        assert_eq!(config.default_ceiling_seconds, 180);
117        assert_eq!(config.pty_ceiling_seconds, 300);
118        assert_eq!(config.mcp_ceiling_seconds, 120);
119        assert_eq!(config.streaming_ceiling_seconds, 600);
120        assert_eq!(config.warning_threshold_percent, 80);
121        assert!(config.validate().is_ok());
122    }
123
124    #[test]
125    fn zero_ceiling_disables_limit() {
126        let mut config = TimeoutsConfig::default();
127        config.default_ceiling_seconds = 0;
128        assert!(config.validate().is_ok());
129        assert!(
130            config
131                .ceiling_duration(config.default_ceiling_seconds)
132                .is_none()
133        );
134    }
135
136    #[test]
137    fn warning_threshold_bounds_are_enforced() {
138        let mut config = TimeoutsConfig::default();
139        config.warning_threshold_percent = 0;
140        assert!(config.validate().is_err());
141
142        config.warning_threshold_percent = 100;
143        assert!(config.validate().is_err());
144    }
145}