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(&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 let result = run(&tool, json!({"seconds": 0.0})).await;
131 assert!(result.success);
132
133 let result = run(&tool, json!({"seconds": 0.001})).await;
135 assert!(result.success);
136
137 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 assert!(result.result.contains(" ()"));
295 }
296}