Skip to main content

bamboo_tools/tools/
sleep.rs

1use async_trait::async_trait;
2use bamboo_agent_core::{Tool, ToolError, ToolResult};
3use serde::Deserialize;
4use serde_json::json;
5use tokio::time::{sleep, Duration};
6
7const MAX_SLEEP_SECONDS: f64 = 300.0;
8
9#[derive(Debug, Deserialize)]
10struct SleepArgs {
11    seconds: f64,
12    #[serde(default)]
13    reason: Option<String>,
14}
15
16/// Pause execution for a short duration.
17pub struct SleepTool;
18
19impl SleepTool {
20    pub fn new() -> Self {
21        Self
22    }
23}
24
25impl Default for SleepTool {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31#[async_trait]
32impl Tool for SleepTool {
33    fn name(&self) -> &str {
34        "Sleep"
35    }
36
37    fn description(&self) -> &str {
38        "Pause execution for a specified number of seconds (max 300s)"
39    }
40
41    fn mutability(&self) -> crate::ToolMutability {
42        crate::ToolMutability::ReadOnly
43    }
44
45    fn concurrency_safe(&self) -> bool {
46        true
47    }
48
49    fn parameters_schema(&self) -> serde_json::Value {
50        json!({
51            "type": "object",
52            "properties": {
53                "seconds": {
54                    "type": "number",
55                    "description": "Seconds to sleep, can be fractional"
56                },
57                "reason": {
58                    "type": "string",
59                    "description": "Optional reason for logging"
60                }
61            },
62            "required": ["seconds"],
63            "additionalProperties": false
64        })
65    }
66
67    async fn execute(&self, args: serde_json::Value) -> Result<ToolResult, ToolError> {
68        let parsed: SleepArgs = serde_json::from_value(args)
69            .map_err(|e| ToolError::InvalidArguments(format!("Invalid sleep args: {e}")))?;
70
71        if parsed.seconds < 0.0 {
72            return Err(ToolError::InvalidArguments(
73                "seconds cannot be negative".to_string(),
74            ));
75        }
76        if parsed.seconds > MAX_SLEEP_SECONDS {
77            return Err(ToolError::InvalidArguments(format!(
78                "seconds cannot exceed {MAX_SLEEP_SECONDS}"
79            )));
80        }
81
82        if let Some(reason) = parsed.reason.as_deref() {
83            tracing::info!("Sleeping for {} seconds: {}", parsed.seconds, reason);
84        } else {
85            tracing::info!("Sleeping for {} seconds", parsed.seconds);
86        }
87
88        sleep(Duration::from_secs_f64(parsed.seconds)).await;
89
90        Ok(ToolResult {
91            success: true,
92            result: format!(
93                "Slept for {} seconds{}",
94                parsed.seconds,
95                parsed
96                    .reason
97                    .as_deref()
98                    .map(|r| format!(" ({r})"))
99                    .unwrap_or_default()
100            ),
101            display_preference: None,
102            images: Vec::new(),
103        })
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use std::time::Instant;
111
112    #[tokio::test]
113    async fn sleep_tool_waits_and_returns_success() {
114        let tool = SleepTool::new();
115        let start = Instant::now();
116        let result = tool.execute(json!({"seconds": 0.01})).await.unwrap();
117        assert!(result.success);
118        assert!(start.elapsed().as_millis() >= 10);
119    }
120
121    #[tokio::test(start_paused = true)]
122    async fn sleep_tool_accepts_valid_seconds() {
123        let tool = SleepTool::new();
124
125        // Minimum value
126        let result = tool.execute(json!({"seconds": 0.0})).await.unwrap();
127        assert!(result.success);
128
129        // Small positive value
130        let result = tool.execute(json!({"seconds": 0.001})).await.unwrap();
131        assert!(result.success);
132
133        // Maximum allowed value (300.0)
134        let result = tool.execute(json!({"seconds": 300.0})).await.unwrap();
135        assert!(result.success);
136    }
137
138    #[tokio::test]
139    async fn sleep_tool_rejects_negative_seconds() {
140        let tool = SleepTool::new();
141        let result = tool.execute(json!({"seconds": -1.0})).await;
142        assert!(result.is_err());
143        let error = result.unwrap_err();
144        assert!(matches!(error, ToolError::InvalidArguments(_)));
145    }
146
147    #[tokio::test]
148    async fn sleep_tool_rejects_seconds_exceeding_max() {
149        let tool = SleepTool::new();
150        let result = tool.execute(json!({"seconds": 300.1})).await;
151        assert!(result.is_err());
152        let error = result.unwrap_err();
153        if let ToolError::InvalidArguments(msg) = error {
154            assert!(msg.contains("cannot exceed"));
155            assert!(msg.contains("300"));
156        } else {
157            panic!("Expected InvalidArguments error");
158        }
159    }
160
161    #[tokio::test]
162    async fn sleep_tool_includes_reason_in_result() {
163        let tool = SleepTool::new();
164        let result = tool
165            .execute(json!({
166                "seconds": 0.001,
167                "reason": "testing sleep"
168            }))
169            .await
170            .unwrap();
171
172        assert!(result.success);
173        assert!(result.result.contains("testing sleep"));
174        assert!(result.result.contains("(testing sleep)"));
175    }
176
177    #[tokio::test]
178    async fn sleep_tool_works_without_reason() {
179        let tool = SleepTool::new();
180        let result = tool.execute(json!({"seconds": 0.001})).await.unwrap();
181
182        assert!(result.success);
183        assert!(result.result.contains("Slept for 0.001 seconds"));
184        assert!(!result.result.contains("("));
185    }
186
187    #[tokio::test]
188    async fn sleep_tool_rejects_missing_seconds() {
189        let tool = SleepTool::new();
190        let result = tool.execute(json!({})).await;
191        assert!(result.is_err());
192    }
193
194    #[tokio::test]
195    async fn sleep_tool_rejects_invalid_seconds_type() {
196        let tool = SleepTool::new();
197        let result = tool.execute(json!({"seconds": "not a number"})).await;
198        assert!(result.is_err());
199    }
200
201    #[tokio::test]
202    async fn sleep_tool_accepts_fractional_seconds() {
203        let tool = SleepTool::new();
204        let start = Instant::now();
205        let result = tool.execute(json!({"seconds": 0.05})).await.unwrap();
206
207        assert!(result.success);
208        assert!(result.result.contains("0.05"));
209        assert!(start.elapsed().as_millis() >= 50);
210    }
211
212    #[test]
213    fn sleep_tool_has_correct_name() {
214        let tool = SleepTool::new();
215        assert_eq!(tool.name(), "Sleep");
216    }
217
218    #[test]
219    fn sleep_tool_has_description() {
220        let tool = SleepTool::new();
221        assert!(!tool.description().is_empty());
222        assert!(tool.description().contains("300"));
223    }
224
225    #[test]
226    fn sleep_tool_parameters_schema_has_required_fields() {
227        let tool = SleepTool::new();
228        let schema = tool.parameters_schema();
229
230        assert_eq!(schema["type"], "object");
231        assert!(schema["properties"]["seconds"].is_object());
232        assert!(schema["properties"]["reason"].is_object());
233        assert!(schema["required"]
234            .as_array()
235            .unwrap()
236            .contains(&json!("seconds")));
237    }
238
239    #[tokio::test]
240    async fn sleep_tool_default_impl() {
241        let tool = SleepTool;
242        let result = tool.execute(json!({"seconds": 0.001})).await.unwrap();
243        assert!(result.success);
244    }
245
246    #[tokio::test]
247    async fn sleep_tool_handles_zero_seconds() {
248        let tool = SleepTool::new();
249        let result = tool.execute(json!({"seconds": 0.0})).await.unwrap();
250
251        assert!(result.success);
252        assert!(result.result.contains("0 seconds"));
253    }
254
255    #[tokio::test]
256    async fn sleep_tool_reason_with_special_characters() {
257        let tool = SleepTool::new();
258        let result = tool
259            .execute(json!({
260                "seconds": 0.001,
261                "reason": "等待数据 🎯 (waiting for data)"
262            }))
263            .await
264            .unwrap();
265
266        assert!(result.success);
267        assert!(result.result.contains("等待数据 🎯"));
268    }
269
270    #[tokio::test]
271    async fn sleep_tool_reason_empty_string() {
272        let tool = SleepTool::new();
273        let result = tool
274            .execute(json!({
275                "seconds": 0.001,
276                "reason": ""
277            }))
278            .await
279            .unwrap();
280
281        assert!(result.success);
282        // Empty string is still treated as a reason, so it will show " ()"
283        assert!(result.result.contains(" ()"));
284    }
285}