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