PicoBot/docs/superpowers/plans/2026-07-28-sleep-tool.md

6.7 KiB
Raw Blame History

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 086400 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.