6.7 KiB
Sleep Tool Implementation Plan
For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add a model-callable sleep tool that asynchronously waits for 0~86400 whole seconds and terminates cleanly when its Turn is cancelled.
Architecture: A focused, stateless SleepTool validates its single argument and waits on one bounded Tokio timer. The existing default registry exposes it to agents, dropping the surrounding execution future cancels the timer, and terminal Turn reduction marks active tool blocks cancelled.
Tech Stack: Rust 2024, Tokio timers and paused-time tests, existing Tool/ToolResult interfaces, Serde JSON.
Chunk 1: Tool And Registration
Task 1: Implement And Register SleepTool
Files:
-
Create:
src/tools/sleep.rs -
Modify:
src/tools/mod.rs:1-52 -
Modify:
src/tools/mod.rs:74-90 -
Test:
src/tools/sleep.rs -
Step 1: Declare the module and write failing metadata tests
Add pub mod sleep; and pub use sleep::SleepTool; to src/tools/mod.rs. Create src/tools/sleep.rs with a test module that imports super::*, crate::tools::Tool, serde_json::json, and std::time::Duration. Add a synchronous test asserting the name is sleep, the schema requires seconds, and its property type is integer with minimum 0.
- Step 2: Write failing validation tests
Add a zero-second async test asserting exact successful output Slept for 0 second(s).. Add a table-driven async test for {}, negative, fractional, string, large float, and 86401; assert each result is unsuccessful with empty output and a populated error. Add a parser boundary test:
#[test]
fn accepts_24_hour_boundary() {
assert_eq!(parse_seconds(&json!({"seconds": 86_400})), Ok(86_400));
}
- Step 3: Write failing timer and cancellation tests
Add the following paused-clock elapsed test. Yield after every advance so expired timers are polled deterministically:
#[tokio::test(start_paused = true)]
async fn waits_for_requested_seconds() {
let handle = tokio::spawn(async { SleepTool::new().execute(json!({"seconds": 2})).await });
tokio::task::yield_now().await;
tokio::time::advance(Duration::from_secs(1)).await;
tokio::task::yield_now().await;
assert!(!handle.is_finished());
tokio::time::advance(Duration::from_secs(1)).await;
tokio::task::yield_now().await;
assert!(handle.await.unwrap().unwrap().success);
}
Add a 24-hour boundary test that advances to one second before the deadline, asserts the handle is unfinished, advances the final second, and asserts success.
Add the cancellation test, which yields before aborting to ensure the timer has been registered:
#[tokio::test(start_paused = true)]
async fn cancellation_drops_an_active_sleep() {
let handle = tokio::spawn(async {
SleepTool::new()
.execute(json!({"seconds": MAX_SLEEP_SECONDS}))
.await
});
tokio::task::yield_now().await;
assert!(!handle.is_finished());
handle.abort();
assert!(handle.await.unwrap_err().is_cancelled());
}
- Step 4: Run the focused test target and confirm RED
Run: cargo test --lib tools::sleep::tests
Expected: compilation fails because SleepTool, parse_seconds, and MAX_SLEEP_SECONDS are not defined.
- Step 5: Implement the minimal tool
Implement src/tools/sleep.rs with this shape:
use super::traits::{Tool, ToolResult};
use async_trait::async_trait;
use serde_json::json;
use std::time::Duration;
const MAX_SLEEP_SECONDS: u64 = 86_400;
pub struct SleepTool;
impl SleepTool {
pub fn new() -> Self {
Self
}
}
impl Default for SleepTool {
fn default() -> Self {
Self::new()
}
}
fn parse_seconds(args: &serde_json::Value) -> Result<u64, String> {
let seconds = args.get("seconds")
.and_then(serde_json::Value::as_u64)
.ok_or_else(|| "seconds must be a non-negative integer".to_string())?;
if seconds > MAX_SLEEP_SECONDS {
return Err("seconds must not exceed 86400 (24 hours)".to_string());
}
Ok(seconds)
}
#[async_trait]
impl Tool for SleepTool {
fn name(&self) -> &str {
"sleep"
}
fn description(&self) -> &str {
"Pause the current agent execution for a specified number of whole seconds."
}
fn parameters_schema(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {
"seconds": {
"type": "integer",
"minimum": 0,
"maximum": MAX_SLEEP_SECONDS,
"description": "Number of whole seconds to wait, up to 24 hours."
}
},
"required": ["seconds"]
})
}
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
let seconds = match parse_seconds(&args) {
Ok(seconds) => seconds,
Err(error) => {
return Ok(ToolResult {
success: false,
output: String::new(),
error: Some(error),
});
}
};
tokio::time::sleep(Duration::from_secs(seconds)).await;
Ok(ToolResult {
success: true,
output: format!("Slept for {seconds} second(s)."),
error: None,
})
}
}
Keep the default read_only, concurrency_safe, and exclusive methods unchanged so a batch containing sleep executes sequentially.
- Step 6: Register the tool
In create_default_tools, add:
registry.register(SleepTool::new());
Place it with the other stateless core tools, immediately after CalculatorTool.
- Step 7: Run focused tests and confirm GREEN
Run: cargo test --lib tools::sleep::tests
Expected: all sleep tests pass, including paused-time and cancellation cases.
- Step 8: Verify the complete Rust change
Run: cargo test --lib
Expected: all library tests pass.
Run: cargo clippy --all-targets --all-features -- -D warnings
Expected: exits successfully with no warnings.
Run: cargo build
Expected: debug build succeeds, including embedded WebUI build handling.
- Step 9: Inspect the final diff
Run: git diff --check && git status --short && git diff -- src/tools/sleep.rs src/tools/mod.rs docs/superpowers/specs/2026-07-28-sleep-tool-design.md docs/superpowers/plans/2026-07-28-sleep-tool.md
Expected: no whitespace errors; only the intended sleep implementation, Turn cancellation handling, public documentation, tests, and patch-version files are changed. Do not commit unless the user explicitly requests it.