bamboo_tools/tools/
sleep.rs1use 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
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 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 let result = run(&tool, json!({"seconds": 0.0})).await;
135 assert!(result.success);
136
137 let result = run(&tool, json!({"seconds": 0.001})).await;
139 assert!(result.success);
140
141 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 assert!(result.result.contains(" ()"));
301 }
302}