1use std::path::PathBuf;
5
6use schemars::JsonSchema;
7use serde::Deserialize;
8
9use crate::executor::{
10 ClaimSource, ToolCall, ToolError, ToolExecutor, ToolOutput, deserialize_params,
11};
12use crate::registry::{InvocationHint, ToolDef};
13
14const TOOL_NAME: &str = "set_working_directory";
15
16const TOOL_DESCRIPTION: &str = "Change the agent's working directory. \
17Shell commands (`bash`) run in child processes — a `cd` inside them does NOT persist. \
18Use this tool when you need to change the working context for subsequent operations. \
19Returns the new absolute working directory path on success.";
20
21#[derive(Deserialize, JsonSchema)]
22struct SetCwdParams {
23 path: String,
25}
26
27#[derive(Debug, Default)]
33pub struct SetCwdExecutor;
34
35impl ToolExecutor for SetCwdExecutor {
36 async fn execute_tool_call(&self, call: &ToolCall) -> Result<Option<ToolOutput>, ToolError> {
37 if call.tool_id != TOOL_NAME {
38 return Ok(None);
39 }
40 let params: SetCwdParams = deserialize_params(&call.params)?;
41 let target = PathBuf::from(¶ms.path);
42
43 let resolved = if target.is_absolute() {
45 target
46 } else {
47 std::env::current_dir()
48 .map_err(ToolError::Execution)?
49 .join(target)
50 };
51
52 std::env::set_current_dir(&resolved).map_err(ToolError::Execution)?;
53
54 let new_cwd = std::env::current_dir().map_err(ToolError::Execution)?;
55 let summary = new_cwd.display().to_string();
56
57 Ok(Some(ToolOutput {
58 tool_name: TOOL_NAME.to_owned(),
59 summary,
60 blocks_executed: 1,
61 filter_stats: None,
62 diff: None,
63 streamed: false,
64 terminal_id: None,
65 locations: None,
66 raw_response: None,
67 claim_source: Some(ClaimSource::FileSystem),
68 }))
69 }
70
71 fn tool_definitions(&self) -> Vec<ToolDef> {
72 vec![ToolDef {
73 id: TOOL_NAME.into(),
74 description: TOOL_DESCRIPTION.into(),
75 schema: schemars::schema_for!(SetCwdParams),
76 invocation: InvocationHint::ToolCall,
77 }]
78 }
79
80 fn is_tool_retryable(&self, _tool_id: &str) -> bool {
81 false
82 }
83
84 async fn execute(&self, _response: &str) -> Result<Option<ToolOutput>, ToolError> {
85 Ok(None)
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 fn make_call(path: &str) -> ToolCall {
94 let mut params = serde_json::Map::new();
95 params.insert(
96 "path".to_owned(),
97 serde_json::Value::String(path.to_owned()),
98 );
99 ToolCall {
100 tool_id: TOOL_NAME.to_owned(),
101 params,
102 caller_id: None,
103 }
104 }
105
106 #[tokio::test]
107 async fn set_cwd_changes_process_cwd() {
108 let original_cwd = std::env::current_dir().unwrap();
109 let dir = tempfile::tempdir().unwrap();
110 let executor = SetCwdExecutor;
111 let call = make_call(dir.path().to_str().unwrap());
112 let result = executor.execute_tool_call(&call).await.unwrap();
113 assert!(result.is_some());
114 let out = result.unwrap();
115 let new_cwd = std::env::current_dir().unwrap();
117 assert_eq!(out.summary, new_cwd.display().to_string());
118 let _ = std::env::set_current_dir(&original_cwd);
120 }
121
122 #[tokio::test]
123 async fn set_cwd_returns_none_for_unknown_tool() {
124 let executor = SetCwdExecutor;
125 let call = ToolCall {
126 tool_id: "other_tool".to_owned(),
127 params: serde_json::Map::new(),
128 caller_id: None,
129 };
130 let result = executor.execute_tool_call(&call).await.unwrap();
131 assert!(result.is_none());
132 }
133
134 #[tokio::test]
135 async fn set_cwd_errors_on_nonexistent_path() {
136 let executor = SetCwdExecutor;
137 let call = make_call("/nonexistent/path/that/does/not/exist");
138 let result = executor.execute_tool_call(&call).await;
139 assert!(result.is_err());
140 }
141
142 #[test]
143 fn tool_definitions_contains_set_working_directory() {
144 let executor = SetCwdExecutor;
145 let defs = executor.tool_definitions();
146 assert_eq!(defs.len(), 1);
147 assert_eq!(defs[0].id.as_ref(), TOOL_NAME);
148 }
149}