Skip to main content

bamboo_tools/tools/
sleep.rs

1use async_trait::async_trait;
2use bamboo_agent_core::{Tool, ToolClass, ToolCtx, ToolError, ToolOutcome, 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 classify(&self, _args: &serde_json::Value) -> ToolClass {
42        ToolClass::READONLY_PARALLEL.promotable()
43    }
44
45    fn parameters_schema(&self) -> serde_json::Value {
46        json!({
47            "type": "object",
48            "properties": {
49                "seconds": {
50                    "type": "number",
51                    "description": "Seconds to sleep, can be fractional"
52                },
53                "reason": {
54                    "type": "string",
55                    "description": "Optional reason for logging"
56                }
57            },
58            "required": ["seconds"],
59            "additionalProperties": false
60        })
61    }
62
63    async fn invoke(
64        &self,
65        args: serde_json::Value,
66        _ctx: ToolCtx,
67    ) -> Result<ToolOutcome, 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(ToolOutcome::Completed(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    async fn run(tool: &SleepTool, args: serde_json::Value) -> ToolResult {
113        let out = tool.invoke(args, ToolCtx::none("t")).await.unwrap();
114        let ToolOutcome::Completed(r) = out else {
115            panic!("expected Completed")
116        };
117        r
118    }
119
120    #[tokio::test]
121    async fn sleep_tool_waits_and_returns_success() {
122        let tool = SleepTool::new();
123        let start = Instant::now();
124        let result = run(&tool, json!({"seconds": 0.01})).await;
125        assert!(result.success);
126        assert!(start.elapsed().as_millis() >= 10);
127    }
128
129    #[tokio::test(start_paused = true)]
130    async fn sleep_tool_accepts_valid_seconds() {
131        let tool = SleepTool::new();
132
133        // Minimum value
134        let result = run(&tool, json!({"seconds": 0.0})).await;
135        assert!(result.success);
136
137        // Small positive value
138        let result = run(&tool, json!({"seconds": 0.001})).await;
139        assert!(result.success);
140
141        // Maximum allowed value (300.0)
142        let result = run(&tool, json!({"seconds": 300.0})).await;
143        assert!(result.success);
144    }
145
146    #[tokio::test]
147    async fn sleep_tool_rejects_negative_seconds() {
148        let tool = SleepTool::new();
149        let result = tool
150            .invoke(json!({"seconds": -1.0}), ToolCtx::none("t"))
151            .await;
152        assert!(result.is_err());
153        let error = result.unwrap_err();
154        assert!(matches!(error, ToolError::InvalidArguments(_)));
155    }
156
157    #[tokio::test]
158    async fn sleep_tool_rejects_seconds_exceeding_max() {
159        let tool = SleepTool::new();
160        let result = tool
161            .invoke(json!({"seconds": 300.1}), ToolCtx::none("t"))
162            .await;
163        assert!(result.is_err());
164        let error = result.unwrap_err();
165        if let ToolError::InvalidArguments(msg) = error {
166            assert!(msg.contains("cannot exceed"));
167            assert!(msg.contains("300"));
168        } else {
169            panic!("Expected InvalidArguments error");
170        }
171    }
172
173    #[tokio::test]
174    async fn sleep_tool_includes_reason_in_result() {
175        let tool = SleepTool::new();
176        let result = run(
177            &tool,
178            json!({
179                "seconds": 0.001,
180                "reason": "testing sleep"
181            }),
182        )
183        .await;
184
185        assert!(result.success);
186        assert!(result.result.contains("testing sleep"));
187        assert!(result.result.contains("(testing sleep)"));
188    }
189
190    #[tokio::test]
191    async fn sleep_tool_works_without_reason() {
192        let tool = SleepTool::new();
193        let result = run(&tool, json!({"seconds": 0.001})).await;
194
195        assert!(result.success);
196        assert!(result.result.contains("Slept for 0.001 seconds"));
197        assert!(!result.result.contains("("));
198    }
199
200    #[tokio::test]
201    async fn sleep_tool_rejects_missing_seconds() {
202        let tool = SleepTool::new();
203        let result = tool.invoke(json!({}), ToolCtx::none("t")).await;
204        assert!(result.is_err());
205    }
206
207    #[tokio::test]
208    async fn sleep_tool_rejects_invalid_seconds_type() {
209        let tool = SleepTool::new();
210        let result = tool
211            .invoke(json!({"seconds": "not a number"}), ToolCtx::none("t"))
212            .await;
213        assert!(result.is_err());
214    }
215
216    #[tokio::test]
217    async fn sleep_tool_accepts_fractional_seconds() {
218        let tool = SleepTool::new();
219        let start = Instant::now();
220        let result = run(&tool, json!({"seconds": 0.05})).await;
221
222        assert!(result.success);
223        assert!(result.result.contains("0.05"));
224        assert!(start.elapsed().as_millis() >= 50);
225    }
226
227    #[test]
228    fn sleep_tool_has_correct_name() {
229        let tool = SleepTool::new();
230        assert_eq!(tool.name(), "Sleep");
231    }
232
233    #[test]
234    fn sleep_tool_has_description() {
235        let tool = SleepTool::new();
236        assert!(!tool.description().is_empty());
237        assert!(tool.description().contains("300"));
238    }
239
240    #[test]
241    fn sleep_tool_parameters_schema_has_required_fields() {
242        let tool = SleepTool::new();
243        let schema = tool.parameters_schema();
244
245        assert_eq!(schema["type"], "object");
246        assert!(schema["properties"]["seconds"].is_object());
247        assert!(schema["properties"]["reason"].is_object());
248        assert!(schema["required"]
249            .as_array()
250            .unwrap()
251            .contains(&json!("seconds")));
252    }
253
254    #[tokio::test]
255    async fn sleep_tool_default_impl() {
256        let tool = SleepTool;
257        let result = run(&tool, json!({"seconds": 0.001})).await;
258        assert!(result.success);
259    }
260
261    #[tokio::test]
262    async fn sleep_tool_handles_zero_seconds() {
263        let tool = SleepTool::new();
264        let result = run(&tool, json!({"seconds": 0.0})).await;
265
266        assert!(result.success);
267        assert!(result.result.contains("0 seconds"));
268    }
269
270    #[tokio::test]
271    async fn sleep_tool_reason_with_special_characters() {
272        let tool = SleepTool::new();
273        let result = run(
274            &tool,
275            json!({
276                "seconds": 0.001,
277                "reason": "等待数据 🎯 (waiting for data)"
278            }),
279        )
280        .await;
281
282        assert!(result.success);
283        assert!(result.result.contains("等待数据 🎯"));
284    }
285
286    #[tokio::test]
287    async fn sleep_tool_reason_empty_string() {
288        let tool = SleepTool::new();
289        let result = run(
290            &tool,
291            json!({
292                "seconds": 0.001,
293                "reason": ""
294            }),
295        )
296        .await;
297
298        assert!(result.success);
299        // Empty string is still treated as a reason, so it will show " ()"
300        assert!(result.result.contains(" ()"));
301    }
302}