Skip to main content

thulp_skills/
config.rs

1//! Configuration types for skill execution.
2//!
3//! This module provides configuration for timeouts and retries during skill execution.
4
5use std::time::Duration;
6
7/// Configuration for execution timeouts.
8#[derive(Debug, Clone)]
9pub struct TimeoutConfig {
10    /// Maximum time for entire skill execution.
11    pub skill_timeout: Duration,
12
13    /// Maximum time for a single step.
14    pub step_timeout: Duration,
15
16    /// Maximum time for a single tool call.
17    pub tool_timeout: Duration,
18
19    /// Action to take on timeout.
20    pub timeout_action: TimeoutAction,
21}
22
23/// Action to take when a timeout occurs.
24#[derive(Debug, Clone, Default, PartialEq, Eq)]
25pub enum TimeoutAction {
26    /// Fail immediately with an error.
27    #[default]
28    Fail,
29
30    /// Skip the timed-out step and continue execution.
31    Skip,
32
33    /// Return partial results collected so far.
34    Partial,
35}
36
37impl Default for TimeoutConfig {
38    fn default() -> Self {
39        Self {
40            skill_timeout: Duration::from_secs(300), // 5 minutes
41            step_timeout: Duration::from_secs(60),   // 1 minute
42            tool_timeout: Duration::from_secs(30),   // 30 seconds
43            timeout_action: TimeoutAction::Fail,
44        }
45    }
46}
47
48impl TimeoutConfig {
49    /// Create a new timeout configuration with default values.
50    pub fn new() -> Self {
51        Self::default()
52    }
53
54    /// Set the skill timeout.
55    pub fn with_skill_timeout(mut self, timeout: Duration) -> Self {
56        self.skill_timeout = timeout;
57        self
58    }
59
60    /// Set the step timeout.
61    pub fn with_step_timeout(mut self, timeout: Duration) -> Self {
62        self.step_timeout = timeout;
63        self
64    }
65
66    /// Set the tool timeout.
67    pub fn with_tool_timeout(mut self, timeout: Duration) -> Self {
68        self.tool_timeout = timeout;
69        self
70    }
71
72    /// Set the timeout action.
73    pub fn with_timeout_action(mut self, action: TimeoutAction) -> Self {
74        self.timeout_action = action;
75        self
76    }
77}
78
79/// Configuration for retry behavior.
80#[derive(Debug, Clone)]
81pub struct RetryConfig {
82    /// Maximum number of retry attempts.
83    pub max_retries: usize,
84
85    /// Initial delay between retries.
86    pub initial_delay: Duration,
87
88    /// Maximum delay between retries (caps exponential backoff).
89    pub max_delay: Duration,
90
91    /// Backoff strategy to use.
92    pub backoff: BackoffStrategy,
93
94    /// Which error types are retryable.
95    pub retryable_errors: Vec<RetryableError>,
96}
97
98/// Strategy for calculating delay between retries.
99#[derive(Debug, Clone, Default, PartialEq, Eq)]
100pub enum BackoffStrategy {
101    /// Fixed delay between retries.
102    Fixed,
103
104    /// Exponential backoff (delay * 2^attempt).
105    Exponential,
106
107    /// Exponential backoff with random jitter.
108    #[default]
109    ExponentialJitter,
110}
111
112/// Types of errors that can be retried.
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum RetryableError {
115    /// Network or connection errors.
116    Network,
117
118    /// Rate limit errors (HTTP 429).
119    RateLimit,
120
121    /// Timeout errors.
122    Timeout,
123
124    /// Server errors (HTTP 5xx).
125    ServerError,
126
127    /// All errors are retryable.
128    All,
129}
130
131impl Default for RetryConfig {
132    fn default() -> Self {
133        Self {
134            max_retries: 3,
135            initial_delay: Duration::from_millis(100),
136            max_delay: Duration::from_secs(10),
137            backoff: BackoffStrategy::ExponentialJitter,
138            retryable_errors: vec![
139                RetryableError::Network,
140                RetryableError::RateLimit,
141                RetryableError::Timeout,
142            ],
143        }
144    }
145}
146
147impl RetryConfig {
148    /// Create a new retry configuration with default values.
149    pub fn new() -> Self {
150        Self::default()
151    }
152
153    /// Disable retries.
154    pub fn no_retries() -> Self {
155        Self {
156            max_retries: 0,
157            ..Default::default()
158        }
159    }
160
161    /// Set the maximum number of retries.
162    pub fn with_max_retries(mut self, max: usize) -> Self {
163        self.max_retries = max;
164        self
165    }
166
167    /// Set the initial delay.
168    pub fn with_initial_delay(mut self, delay: Duration) -> Self {
169        self.initial_delay = delay;
170        self
171    }
172
173    /// Set the maximum delay.
174    pub fn with_max_delay(mut self, delay: Duration) -> Self {
175        self.max_delay = delay;
176        self
177    }
178
179    /// Set the backoff strategy.
180    pub fn with_backoff(mut self, strategy: BackoffStrategy) -> Self {
181        self.backoff = strategy;
182        self
183    }
184
185    /// Set which errors are retryable.
186    pub fn with_retryable_errors(mut self, errors: Vec<RetryableError>) -> Self {
187        self.retryable_errors = errors;
188        self
189    }
190
191    /// Make all errors retryable.
192    pub fn retry_all_errors(mut self) -> Self {
193        self.retryable_errors = vec![RetryableError::All];
194        self
195    }
196}
197
198/// Combined execution configuration.
199#[derive(Debug, Clone, Default)]
200pub struct ExecutionConfig {
201    /// Timeout configuration.
202    pub timeout: TimeoutConfig,
203
204    /// Retry configuration.
205    pub retry: RetryConfig,
206}
207
208impl ExecutionConfig {
209    /// Create a new execution configuration with defaults.
210    pub fn new() -> Self {
211        Self::default()
212    }
213
214    /// Set timeout configuration.
215    pub fn with_timeout(mut self, config: TimeoutConfig) -> Self {
216        self.timeout = config;
217        self
218    }
219
220    /// Set retry configuration.
221    pub fn with_retry(mut self, config: RetryConfig) -> Self {
222        self.retry = config;
223        self
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn test_timeout_config_defaults() {
233        let config = TimeoutConfig::default();
234        assert_eq!(config.skill_timeout, Duration::from_secs(300));
235        assert_eq!(config.step_timeout, Duration::from_secs(60));
236        assert_eq!(config.tool_timeout, Duration::from_secs(30));
237        assert_eq!(config.timeout_action, TimeoutAction::Fail);
238    }
239
240    #[test]
241    fn test_timeout_config_builder() {
242        let config = TimeoutConfig::new()
243            .with_skill_timeout(Duration::from_secs(120))
244            .with_step_timeout(Duration::from_secs(30))
245            .with_timeout_action(TimeoutAction::Skip);
246
247        assert_eq!(config.skill_timeout, Duration::from_secs(120));
248        assert_eq!(config.step_timeout, Duration::from_secs(30));
249        assert_eq!(config.timeout_action, TimeoutAction::Skip);
250    }
251
252    #[test]
253    fn test_retry_config_defaults() {
254        let config = RetryConfig::default();
255        assert_eq!(config.max_retries, 3);
256        assert_eq!(config.initial_delay, Duration::from_millis(100));
257        assert_eq!(config.backoff, BackoffStrategy::ExponentialJitter);
258        assert!(config.retryable_errors.contains(&RetryableError::Network));
259    }
260
261    #[test]
262    fn test_retry_config_no_retries() {
263        let config = RetryConfig::no_retries();
264        assert_eq!(config.max_retries, 0);
265    }
266
267    #[test]
268    fn test_retry_config_builder() {
269        let config = RetryConfig::new()
270            .with_max_retries(5)
271            .with_initial_delay(Duration::from_millis(200))
272            .with_backoff(BackoffStrategy::Fixed)
273            .retry_all_errors();
274
275        assert_eq!(config.max_retries, 5);
276        assert_eq!(config.initial_delay, Duration::from_millis(200));
277        assert_eq!(config.backoff, BackoffStrategy::Fixed);
278        assert!(config.retryable_errors.contains(&RetryableError::All));
279    }
280
281    #[test]
282    fn test_execution_config() {
283        let config = ExecutionConfig::new()
284            .with_timeout(TimeoutConfig::new().with_skill_timeout(Duration::from_secs(60)))
285            .with_retry(RetryConfig::no_retries());
286
287        assert_eq!(config.timeout.skill_timeout, Duration::from_secs(60));
288        assert_eq!(config.retry.max_retries, 0);
289    }
290}