synaps_cli/tools/
watcher_exit.rs1use 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 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 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 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}