recursive/tools/
stop_loop.rs1use 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
29pub 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 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}