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 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 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 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 let tmp_path = super::util::unique_tmp_path(path);
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}