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(&self, args: serde_json::Value, _ctx: ToolCtx) -> Result<ToolOutcome, ToolError> {
64        let parsed: SleepArgs = serde_json::from_value(args)
65            .map_err(|e| ToolError::InvalidArguments(format!("Invalid sleep args: {e}")))?;
66
67        if parsed.seconds < 0.0 {
68            return Err(ToolError::InvalidArguments(
69                "seconds cannot be negative".to_string(),
70            ));
71        }
72        if parsed.seconds > MAX_SLEEP_SECONDS {
73            return Err(ToolError::InvalidArguments(format!(
74                "seconds cannot exceed {MAX_SLEEP_SECONDS}"
75            )));
76        }
77
78        if let Some(reason) = parsed.reason.as_deref() {
79            tracing::info!("Sleeping for {} seconds: {}", parsed.seconds, reason);
80        } else {
81            tracing::info!("Sleeping for {} seconds", parsed.seconds);
82        }
83
84        sleep(Duration::from_secs_f64(parsed.seconds)).await;
85
86        Ok(ToolOutcome::Completed(ToolResult {
87            success: true,
88            result: format!(
89                "Slept for {} seconds{}",
90                parsed.seconds,
91                parsed
92                    .reason
93                    .as_deref()
94                    .map(|r| format!(" ({r})"))
95                    .unwrap_or_default()
96            ),
97            display_preference: None,
98            images: Vec::new(),
99        }))
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use std::time::Instant;
107
108    async fn run(tool: &SleepTool, args: serde_json::Value) -> ToolResult {
109        let out = tool.invoke(args, ToolCtx::none("t")).await.unwrap();
110        let ToolOutcome::Completed(r) = out else {
111            panic!("expected Completed")
112        };
113        r
114    }
115
116    #[tokio::test]
117    async fn sleep_tool_waits_and_returns_success() {
118        let tool = SleepTool::new();
119        let start = Instant::now();
120        let result = run(&tool, json!({"seconds": 0.01})).await;
121        assert!(result.success);
122        assert!(start.elapsed().as_millis() >= 10);
123    }
124
125    #[tokio::test(start_paused = true)]
126    async fn sleep_tool_accepts_valid_seconds() {
127        let tool = SleepTool::new();
128
129        // Minimum value
130        let result = run(&tool, json!({"seconds": 0.0})).await;
131        assert!(result.success);
132
133        // Small positive value
134        let result = run(&tool, json!({"seconds": 0.001})).await;
135        assert!(result.success);
136
137        // Maximum allowed value (300.0)
138        let result = run(&tool, json!({"seconds": 300.0})).await;
139        assert!(result.success);
140    }
141
142    #[tokio::test]
143    async fn sleep_tool_rejects_negative_seconds() {
144        let tool = SleepTool::new();
145        let result = tool.invoke(json!({"seconds": -1.0}), ToolCtx::none("t")).await;
146        assert!(result.is_err());
147        let error = result.unwrap_err();
148        assert!(matches!(error, ToolError::InvalidArguments(_)));
149    }
150
151    #[tokio::test]
152    async fn sleep_tool_rejects_seconds_exceeding_max() {
153        let tool = SleepTool::new();
154        let result = tool
155            .invoke(json!({"seconds": 300.1}), ToolCtx::none("t"))
156            .await;
157        assert!(result.is_err());
158        let error = result.unwrap_err();
159        if let ToolError::InvalidArguments(msg) = error {
160            assert!(msg.contains("cannot exceed"));
161            assert!(msg.contains("300"));
162        } else {
163            panic!("Expected InvalidArguments error");
164        }
165    }
166
167    #[tokio::test]
168    async fn sleep_tool_includes_reason_in_result() {
169        let tool = SleepTool::new();
170        let result = run(
171            &tool,
172            json!({
173                "seconds": 0.001,
174                "reason": "testing sleep"
175            }),
176        )
177        .await;
178
179        assert!(result.success);
180        assert!(result.result.contains("testing sleep"));
181        assert!(result.result.contains("(testing sleep)"));
182    }
183
184    #[tokio::test]
185    async fn sleep_tool_works_without_reason() {
186        let tool = SleepTool::new();
187        let result = run(&tool, json!({"seconds": 0.001})).await;
188
189        assert!(result.success);
190        assert!(result.result.contains("Slept for 0.001 seconds"));
191        assert!(!result.result.contains("("));
192    }
193
194    #[tokio::test]
195    async fn sleep_tool_rejects_missing_seconds() {
196        let tool = SleepTool::new();
197        let result = tool.invoke(json!({}), ToolCtx::none("t")).await;
198        assert!(result.is_err());
199    }
200
201    #[tokio::test]
202    async fn sleep_tool_rejects_invalid_seconds_type() {
203        let tool = SleepTool::new();
204        let result = tool
205            .invoke(json!({"seconds": "not a number"}), ToolCtx::none("t"))
206            .await;
207        assert!(result.is_err());
208    }
209
210    #[tokio::test]
211    async fn sleep_tool_accepts_fractional_seconds() {
212        let tool = SleepTool::new();
213        let start = Instant::now();
214        let result = run(&tool, json!({"seconds": 0.05})).await;
215
216        assert!(result.success);
217        assert!(result.result.contains("0.05"));
218        assert!(start.elapsed().as_millis() >= 50);
219    }
220
221    #[test]
222    fn sleep_tool_has_correct_name() {
223        let tool = SleepTool::new();
224        assert_eq!(tool.name(), "Sleep");
225    }
226
227    #[test]
228    fn sleep_tool_has_description() {
229        let tool = SleepTool::new();
230        assert!(!tool.description().is_empty());
231        assert!(tool.description().contains("300"));
232    }
233
234    #[test]
235    fn sleep_tool_parameters_schema_has_required_fields() {
236        let tool = SleepTool::new();
237        let schema = tool.parameters_schema();
238
239        assert_eq!(schema["type"], "object");
240        assert!(schema["properties"]["seconds"].is_object());
241        assert!(schema["properties"]["reason"].is_object());
242        assert!(schema["required"]
243            .as_array()
244            .unwrap()
245            .contains(&json!("seconds")));
246    }
247
248    #[tokio::test]
249    async fn sleep_tool_default_impl() {
250        let tool = SleepTool;
251        let result = run(&tool, json!({"seconds": 0.001})).await;
252        assert!(result.success);
253    }
254
255    #[tokio::test]
256    async fn sleep_tool_handles_zero_seconds() {
257        let tool = SleepTool::new();
258        let result = run(&tool, json!({"seconds": 0.0})).await;
259
260        assert!(result.success);
261        assert!(result.result.contains("0 seconds"));
262    }
263
264    #[tokio::test]
265    async fn sleep_tool_reason_with_special_characters() {
266        let tool = SleepTool::new();
267        let result = run(
268            &tool,
269            json!({
270                "seconds": 0.001,
271                "reason": "等待数据 🎯 (waiting for data)"
272            }),
273        )
274        .await;
275
276        assert!(result.success);
277        assert!(result.result.contains("等待数据 🎯"));
278    }
279
280    #[tokio::test]
281    async fn sleep_tool_reason_empty_string() {
282        let tool = SleepTool::new();
283        let result = run(
284            &tool,
285            json!({
286                "seconds": 0.001,
287                "reason": ""
288            }),
289        )
290        .await;
291
292        assert!(result.success);
293        // Empty string is still treated as a reason, so it will show " ()"
294        assert!(result.result.contains(" ()"));
295    }
296}