agent_engine/tools/
watcher_exit.rs1use super::{Tool, ToolContext};
2use crate::watcher_types::HandoffState;
3use crate::{Result, RuntimeError};
4use serde_json::{json, Value};
5
6pub 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 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 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 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}