Skip to main content

agent_engine/tools/
watcher_exit.rs

1use super::{Tool, ToolContext};
2use crate::watcher_types::HandoffState;
3use crate::{Result, RuntimeError};
4use serde_json::{json, Value};
5
6/// Start of a successful `watcher_exit` result: the handoff was written
7/// (atomically) before this text was returned. The watcher agent treats
8/// only such a result as a clean exit.
9pub const WATCHER_EXIT_ACK: &str = "Shutdown acknowledged. Handoff saved.";
10
11pub struct WatcherExitTool;
12
13#[async_trait::async_trait]
14impl Tool for WatcherExitTool {
15    fn origin(&self) -> crate::tools::ToolOrigin {
16        crate::tools::ToolOrigin::Builtin
17    }
18
19    fn name(&self) -> &str {
20        "watcher_exit"
21    }
22
23    fn description(&self) -> &str {
24        "Signal that you've completed your work. Call this when you're done or at a natural stopping point. Provide a handoff summary for your next session."
25    }
26
27    fn parameters(&self) -> Value {
28        json!({
29            "type": "object",
30            "properties": {
31                "reason": {
32                    "type": "string",
33                    "description": "why you're exiting"
34                },
35                "summary": {
36                    "type": "string",
37                    "description": "what you accomplished this session"
38                },
39                "pending": {
40                    "type": "array",
41                    "items": { "type": "string" },
42                    "description": "tasks still pending"
43                },
44                "context": {
45                    "type": "object",
46                    "description": "any structured data for next session"
47                }
48            },
49            "required": ["reason", "summary"]
50        })
51    }
52
53    async fn execute(&self, params: Value, ctx: ToolContext) -> Result<String> {
54        let reason = params["reason"]
55            .as_str()
56            .ok_or_else(|| RuntimeError::Tool("Missing reason parameter".to_string()))?;
57        let summary = params["summary"]
58            .as_str()
59            .ok_or_else(|| RuntimeError::Tool("Missing summary parameter".to_string()))?;
60
61        let pending = params["pending"]
62            .as_array()
63            .map(|arr| {
64                arr.iter()
65                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
66                    .collect::<Vec<String>>()
67            })
68            .unwrap_or_default();
69
70        let context = if params["context"].is_null() {
71            serde_json::Value::Null
72        } else {
73            params["context"].clone()
74        };
75
76        let handoff = HandoffState {
77            summary: summary.to_string(),
78            pending,
79            context,
80        };
81
82        // Write handoff state to the specified path if provided
83        if let Some(ref path) = ctx.capabilities.watcher_exit_path {
84            let json_content = serde_json::to_string_pretty(&handoff)
85                .map_err(|e| RuntimeError::Tool(format!("Failed to serialize handoff: {}", e)))?;
86
87            // Ensure parent directory exists
88            if let Some(parent) = path.parent() {
89                if !parent.as_os_str().is_empty() {
90                    tokio::fs::create_dir_all(parent).await.map_err(|e| {
91                        RuntimeError::Tool(format!("Failed to create directories: {}", e))
92                    })?;
93                }
94            }
95
96            // Atomic write for handoff
97            let tmp_path = super::util::unique_tmp_path(path);
98            tokio::fs::write(&tmp_path, &json_content)
99                .await
100                .map_err(|e| {
101                    RuntimeError::Tool(format!("Failed to write handoff temp file: {}", e))
102                })?;
103            tokio::fs::rename(&tmp_path, &path)
104                .await
105                .map_err(|e| RuntimeError::Tool(format!("Failed to rename handoff file: {}", e)))?;
106        }
107
108        Ok(format!("{WATCHER_EXIT_ACK} Reason: {reason}"))
109    }
110}