bamboo_tools/tools/
sleep.rs1use 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
16pub 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 let result = tool.execute(json!({"seconds": 0.0})).await.unwrap();
127 assert!(result.success);
128
129 let result = tool.execute(json!({"seconds": 0.001})).await.unwrap();
131 assert!(result.success);
132
133 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 assert!(result.result.contains(" ()"));
284 }
285}