Skip to main content

nika_engine/runtime/builtin/
sleep.rs

1//! nika:sleep - Pause execution for duration.
2//!
3//! # Parameters
4//!
5//! ```json
6//! {
7//!   "duration": "1s"  // humantime format: 1s, 500ms, 1m30s, etc.
8//! }
9//! ```
10//!
11//! # Returns
12//!
13//! ```json
14//! {
15//!   "slept_for_ms": 1000
16//! }
17//! ```
18//!
19//! # Limits
20//!
21//! Maximum sleep duration is 5 minutes (300 seconds) to prevent workflow blocking.
22//! Attempts to sleep longer will return an error.
23
24use super::BuiltinTool;
25use crate::error::NikaError;
26use serde::{Deserialize, Serialize};
27use std::future::Future;
28use std::pin::Pin;
29use std::time::Duration;
30
31/// Maximum allowed sleep duration (5 minutes)
32///
33/// This limit prevents agents from blocking workflows indefinitely by calling
34/// nika:sleep with extremely long durations like "1000h".
35pub const MAX_SLEEP_DURATION: Duration = Duration::from_secs(5 * 60);
36
37/// Parameters for nika:sleep tool.
38#[derive(Debug, Clone, Deserialize)]
39struct SleepParams {
40    /// Duration string in humantime format (e.g., "1s", "500ms", "1m30s").
41    duration: String,
42}
43
44/// Response from nika:sleep tool.
45#[derive(Debug, Clone, Serialize)]
46struct SleepResponse {
47    /// Actual duration slept in milliseconds.
48    slept_for_ms: u64,
49}
50
51/// nika:sleep builtin tool.
52///
53/// Pauses execution for the specified duration.
54pub struct SleepTool;
55
56impl BuiltinTool for SleepTool {
57    fn name(&self) -> &'static str {
58        "sleep"
59    }
60
61    fn description(&self) -> &'static str {
62        "Pause execution for the specified duration"
63    }
64
65    fn parameters_schema(&self) -> serde_json::Value {
66        // OpenAI-compatible schema with additionalProperties: false
67        serde_json::json!({
68            "type": "object",
69            "properties": {
70                "duration": {
71                    "type": "string",
72                    "description": "Duration to sleep in humantime format (e.g., '1s', '500ms', '1m30s')"
73                }
74            },
75            "required": ["duration"],
76            "additionalProperties": false
77        })
78    }
79
80    fn call<'a>(
81        &'a self,
82        args: String,
83    ) -> Pin<Box<dyn Future<Output = Result<String, NikaError>> + Send + 'a>> {
84        Box::pin(async move {
85            // Parse parameters
86            let params: SleepParams =
87                serde_json::from_str(&args).map_err(|e| NikaError::BuiltinInvalidParams {
88                    tool: "nika:sleep".into(),
89                    reason: format!("Invalid JSON parameters: {}", e),
90                })?;
91
92            // Parse duration using humantime
93            let duration = humantime::parse_duration(&params.duration).map_err(|e| {
94                NikaError::BuiltinInvalidParams {
95                    tool: "nika:sleep".into(),
96                    reason: format!("Invalid duration '{}': {}", params.duration, e),
97                }
98            })?;
99
100            // Enforce maximum sleep duration (Bug fix: prevent indefinite blocking)
101            if duration > MAX_SLEEP_DURATION {
102                return Err(NikaError::BuiltinInvalidParams {
103                    tool: "nika:sleep".into(),
104                    reason: format!(
105                        "Sleep duration '{}' exceeds maximum allowed {} seconds. \
106                         This limit prevents workflows from being blocked indefinitely.",
107                        params.duration,
108                        MAX_SLEEP_DURATION.as_secs()
109                    ),
110                });
111            }
112
113            // Sleep
114            tokio::time::sleep(duration).await;
115
116            // Return response
117            let response = SleepResponse {
118                slept_for_ms: duration.as_millis() as u64,
119            };
120
121            serde_json::to_string(&response).map_err(|e| NikaError::BuiltinToolError {
122                tool: "nika:sleep".into(),
123                reason: format!("Failed to serialize response: {}", e),
124            })
125        })
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_sleep_tool_name() {
135        let tool = SleepTool;
136        assert_eq!(tool.name(), "sleep");
137    }
138
139    #[test]
140    fn test_sleep_tool_description() {
141        let tool = SleepTool;
142        assert!(tool.description().contains("Pause"));
143    }
144
145    #[test]
146    fn test_sleep_tool_schema() {
147        let tool = SleepTool;
148        let schema = tool.parameters_schema();
149        assert_eq!(schema["type"], "object");
150        assert!(schema["properties"]["duration"].is_object());
151        assert!(schema["required"]
152            .as_array()
153            .unwrap()
154            .contains(&serde_json::json!("duration")));
155    }
156
157    #[tokio::test]
158    async fn test_sleep_executes() {
159        let tool = SleepTool;
160        let start = std::time::Instant::now();
161
162        let result = tool.call(r#"{"duration": "10ms"}"#.to_string()).await;
163
164        assert!(result.is_ok());
165        let elapsed = start.elapsed();
166        // Should have slept at least 10ms (allow some tolerance)
167        assert!(elapsed.as_millis() >= 10);
168
169        let response: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
170        assert_eq!(response["slept_for_ms"], 10);
171    }
172
173    #[tokio::test]
174    async fn test_sleep_parses_seconds() {
175        let tool = SleepTool;
176        let result = tool.call(r#"{"duration": "1ms"}"#.to_string()).await;
177
178        assert!(result.is_ok());
179        let response: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
180        assert_eq!(response["slept_for_ms"], 1);
181    }
182
183    #[tokio::test]
184    async fn test_sleep_parses_complex_duration() {
185        // Test humantime's ability to parse complex durations
186        let duration = humantime::parse_duration("1s500ms");
187        assert!(duration.is_ok());
188        assert_eq!(duration.unwrap().as_millis(), 1500);
189    }
190
191    #[tokio::test]
192    async fn test_sleep_invalid_duration() {
193        let tool = SleepTool;
194        let result = tool
195            .call(r#"{"duration": "not-a-duration"}"#.to_string())
196            .await;
197
198        assert!(result.is_err());
199        let err = result.unwrap_err();
200        assert!(err.to_string().contains("Invalid duration"));
201    }
202
203    #[tokio::test]
204    async fn test_sleep_invalid_json() {
205        let tool = SleepTool;
206        let result = tool.call("not json".to_string()).await;
207
208        assert!(result.is_err());
209        let err = result.unwrap_err();
210        assert!(err.to_string().contains("Invalid JSON parameters"));
211    }
212
213    #[tokio::test]
214    async fn test_sleep_missing_duration() {
215        let tool = SleepTool;
216        let result = tool.call(r#"{}"#.to_string()).await;
217
218        assert!(result.is_err());
219        let err = result.unwrap_err();
220        assert!(err.to_string().contains("Invalid JSON parameters"));
221    }
222
223    // ═══════════════════════════════════════════════════════════════
224    // MAX_SLEEP_DURATION Tests
225    // ═══════════════════════════════════════════════════════════════
226
227    #[test]
228    fn test_max_sleep_duration_is_5_minutes() {
229        assert_eq!(MAX_SLEEP_DURATION.as_secs(), 300);
230        assert_eq!(MAX_SLEEP_DURATION, std::time::Duration::from_secs(5 * 60));
231    }
232
233    #[tokio::test]
234    async fn test_sleep_rejects_excessive_duration_hours() {
235        let tool = SleepTool;
236        // 1000 hours would block forever without this check
237        let result = tool.call(r#"{"duration": "1000h"}"#.to_string()).await;
238
239        assert!(result.is_err());
240        let err = result.unwrap_err();
241        let err_str = err.to_string();
242        assert!(err_str.contains("exceeds maximum"));
243        assert!(err_str.contains("300"));
244    }
245
246    #[tokio::test]
247    async fn test_sleep_rejects_6_minutes() {
248        let tool = SleepTool;
249        // 6 minutes exceeds the 5 minute limit
250        let result = tool.call(r#"{"duration": "6m"}"#.to_string()).await;
251
252        assert!(result.is_err());
253        let err = result.unwrap_err();
254        assert!(err.to_string().contains("exceeds maximum"));
255    }
256
257    #[tokio::test]
258    async fn test_sleep_accepts_5_minutes() {
259        let _tool = SleepTool;
260        // 5 minutes is exactly at the limit - should be accepted
261        // We can't actually wait 5 minutes in a test, but we can verify parsing works
262        let duration = humantime::parse_duration("5m").unwrap();
263        assert!(duration <= MAX_SLEEP_DURATION);
264    }
265
266    #[tokio::test]
267    async fn test_sleep_accepts_4_minutes() {
268        let _tool = SleepTool;
269        // 4 minutes is under the limit
270        let duration = humantime::parse_duration("4m").unwrap();
271        assert!(duration < MAX_SLEEP_DURATION);
272    }
273
274    #[tokio::test]
275    async fn test_sleep_accepts_just_under_limit() {
276        let _tool = SleepTool;
277        // 299 seconds is just under 5 minutes
278        let duration = humantime::parse_duration("299s").unwrap();
279        assert!(duration < MAX_SLEEP_DURATION);
280    }
281
282    #[tokio::test]
283    async fn test_sleep_rejects_just_over_limit() {
284        let tool = SleepTool;
285        // 301 seconds is just over 5 minutes
286        let result = tool.call(r#"{"duration": "301s"}"#.to_string()).await;
287
288        assert!(result.is_err());
289        let err = result.unwrap_err();
290        assert!(err.to_string().contains("exceeds maximum"));
291    }
292}