Skip to main content

somatize_runtime/effects/
sleep_handler.rs

1//! Waiting, as an effect.
2
3use somatize_core::effect::{Effect, EffectHandler, EffectResult};
4use somatize_core::error::Result;
5use somatize_core::value::Value;
6
7/// Performs [`Effect::Sleep`].
8///
9/// `Effect::Sleep` was declared, documented as "journaled like anything
10/// else, so a replay does not sleep again", and handled by nobody: a step
11/// that awaited one got "no handler for effect `sleep:1ms`". The variant
12/// existed; the four lines that make it work did not.
13///
14/// And the journaling is the point. A step that backs off for thirty
15/// seconds pays that once — a replay reads the recorded result and moves
16/// on, which is what makes a resumed agentic run cheap.
17#[derive(Debug, Default, Clone, Copy)]
18pub struct SleepHandler;
19
20impl EffectHandler for SleepHandler {
21    fn handles(&self, effect: &Effect) -> bool {
22        matches!(effect, Effect::Sleep(_))
23    }
24
25    fn perform(&self, effect: &Effect) -> Result<EffectResult> {
26        let Effect::Sleep(duration) = effect else {
27            return Err(somatize_core::error::SomaError::Other(
28                "not a sleep effect".into(),
29            ));
30        };
31        std::thread::sleep(*duration);
32        Ok(EffectResult::Node(Value::Empty))
33    }
34}
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39
40    #[test]
41    fn it_sleeps_and_claims_only_sleeps() {
42        let h = SleepHandler;
43        let effect = Effect::Sleep(std::time::Duration::from_millis(5));
44        assert!(h.handles(&effect));
45        assert!(!h.handles(&Effect::Tool {
46            name: "t".into(),
47            args: Value::Empty
48        }));
49
50        let start = std::time::Instant::now();
51        h.perform(&effect).unwrap();
52        assert!(start.elapsed() >= std::time::Duration::from_millis(5));
53    }
54}