Skip to main content

synaps_cli/tools/
watcher_exit.rs

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