Skip to main content

recursive/tools/
stop_loop.rs

1//! `stop_loop` tool — let the agent end the event-driven loop itself.
2//!
3//! In loop mode the agent keeps the loop alive by arming `schedule_wakeup`,
4//! a `watch_file`, or a pending `run_background` job. Until now it had no way
5//! to *stop* the loop — only the user could, via `/loop stop`. That made the
6//! loop lifecycle opaque to the user: they had to know to type `/loop stop`.
7//!
8//! This tool writes a [`LoopControl::Stop`] onto the shared
9//! [`BackgroundJobManager`]. The TUI loop arbiter drains it between turns and
10//! exits loop mode, returning the TUI to normal interactive mode. So when the
11//! supervised command reaches a terminal outcome — or the user says "stop /
12//! exit the loop" in natural language — the agent calls `stop_loop` and the
13//! loop ends cleanly.
14//!
15//! State lives on the shared background-job manager so the arbiter — which
16//! already holds that manager — can read it without a new shared slot.
17
18use async_trait::async_trait;
19use serde_json::{json, Value};
20use std::path::PathBuf;
21use std::sync::Arc;
22use tokio::sync::Mutex;
23
24use super::run_background::{BackgroundJobManager, LoopControl};
25use super::Tool;
26use crate::error::Result;
27use crate::llm::ToolSpec;
28
29/// The `stop_loop` tool: agent-initiated loop shutdown.
30pub struct StopLoop {
31    manager: Arc<Mutex<BackgroundJobManager>>,
32    _root: PathBuf,
33}
34
35impl StopLoop {
36    pub fn new(root: impl Into<PathBuf>, manager: Arc<Mutex<BackgroundJobManager>>) -> Self {
37        Self {
38            manager,
39            _root: root.into(),
40        }
41    }
42}
43
44#[async_trait]
45impl Tool for StopLoop {
46    fn spec(&self) -> ToolSpec {
47        ToolSpec {
48            name: "stop_loop".into(),
49            description: "Stop the event-driven loop and return the TUI to normal \
50                interactive mode. Call this when the work you were looping on has \
51                reached a final outcome (e.g. a supervised command finished and you \
52                have reported its verdict), or when the user asks to stop / exit the \
53                loop in natural language. The loop stops after the current turn. \
54                Takes no arguments."
55                .into(),
56            parameters: json!({
57                "type": "object",
58                "properties": {},
59            }),
60        }
61    }
62
63    fn is_deferred(&self) -> bool {
64        true
65    }
66
67    async fn execute(&self, _args: Value) -> Result<String> {
68        let mut manager = self.manager.lock().await;
69        manager.set_loop_control(LoopControl::Stop);
70        Ok(json!({
71            "stopping": true,
72            "message": "Loop will stop after this turn. Finish your report; the TUI returns to interactive mode when this turn ends."
73        })
74        .to_string())
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    fn mgr() -> Arc<Mutex<BackgroundJobManager>> {
83        Arc::new(Mutex::new(BackgroundJobManager::new()))
84    }
85
86    #[tokio::test]
87    async fn stop_loop_sets_stop_control() {
88        let tool = StopLoop::new("/tmp", mgr());
89        let out = tool.execute(json!({})).await.unwrap();
90        let v: Value = serde_json::from_str(&out).unwrap();
91        assert_eq!(v["stopping"], true);
92    }
93
94    #[tokio::test]
95    async fn stop_loop_control_is_consumed_once() {
96        let m = mgr();
97        StopLoop::new("/tmp", m.clone())
98            .execute(json!({}))
99            .await
100            .unwrap();
101        {
102            let mut g = m.lock().await;
103            assert_eq!(g.take_loop_control(), Some(LoopControl::Stop));
104        }
105        // Second take returns None (consumed).
106        let mut g = m.lock().await;
107        assert_eq!(g.take_loop_control(), None);
108    }
109
110    #[tokio::test]
111    async fn stop_loop_is_deferred() {
112        let tool = StopLoop::new("/tmp", mgr());
113        assert!(tool.is_deferred());
114    }
115}