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

202 lines
6.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:
```rust
#[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:
```rust
#[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:
```rust
#[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:
```rust
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:
```rust
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.