Skip to main content

cascade_agent/tools/
builtin.rs

1//! Built-in tools: echo, list_tools, read_file, write_file, ask_user.
2
3use async_trait::async_trait;
4use serde_json::{json, Value};
5
6use super::{Tool, ToolResult};
7
8// ---------------------------------------------------------------------------
9// EchoTool
10// ---------------------------------------------------------------------------
11
12/// A simple echo tool useful for testing.
13///
14/// Parameters:
15///   - `message` (string, required): the text to echo back.
16#[derive(Debug)]
17pub struct EchoTool;
18
19impl EchoTool {
20    pub fn new() -> Self {
21        Self
22    }
23}
24
25impl Default for EchoTool {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31#[async_trait]
32impl Tool for EchoTool {
33    fn name(&self) -> &str {
34        "echo"
35    }
36
37    fn description(&self) -> &str {
38        "Echoes back the provided message. Useful for testing and debugging."
39    }
40
41    fn parameters_schema(&self) -> Value {
42        json!({
43            "type": "object",
44            "properties": {
45                "message": {
46                    "type": "string",
47                    "description": "The message to echo back."
48                }
49            },
50            "required": ["message"]
51        })
52    }
53
54    async fn execute(&self, args: Value) -> ToolResult {
55        let message = match args.get("message").and_then(|v| v.as_str()) {
56            Some(m) => m.to_owned(),
57            None => return ToolResult::err("Missing required parameter 'message'"),
58        };
59        ToolResult::ok_string(message)
60    }
61}
62
63// ---------------------------------------------------------------------------
64// ListToolsTool
65// ---------------------------------------------------------------------------
66
67/// Lists all registered tools and their descriptions.
68///
69/// Takes no parameters. Returns a JSON array of objects with `name` and `description`.
70#[derive(Debug)]
71pub struct ListToolsTool {
72    tools: Vec<(String, String)>,
73}
74
75impl ListToolsTool {
76    /// Create from a list of tool names.
77    pub fn new(names: Vec<String>, tools: Vec<std::sync::Arc<dyn Tool>>) -> Self {
78        let mut entries: Vec<(String, String)> =
79            names.iter().map(|n| (n.clone(), String::new())).collect();
80
81        // Fill in descriptions from the actual tool objects.
82        for tool in &tools {
83            if let Some(entry) = entries.iter_mut().find(|(name, _)| name == tool.name()) {
84                entry.1 = tool.description().to_owned();
85            }
86        }
87
88        Self { tools: entries }
89    }
90}
91
92#[async_trait]
93impl Tool for ListToolsTool {
94    fn name(&self) -> &str {
95        "list_tools"
96    }
97
98    fn description(&self) -> &str {
99        "Lists all available tools and their descriptions."
100    }
101
102    fn parameters_schema(&self) -> Value {
103        json!({
104            "type": "object",
105            "properties": {}
106        })
107    }
108
109    async fn execute(&self, _args: Value) -> ToolResult {
110        let list: Vec<Value> = self
111            .tools
112            .iter()
113            .map(|(name, desc)| {
114                json!({
115                    "name": name,
116                    "description": desc
117                })
118            })
119            .collect();
120        ToolResult::ok(json!(list))
121    }
122}
123
124// ---------------------------------------------------------------------------
125// ReadFileTool
126// ---------------------------------------------------------------------------
127
128/// Reads a file from disk and returns its contents.
129///
130/// Parameters:
131///   - `path` (string, required): filesystem path to the file.
132#[derive(Debug)]
133pub struct ReadFileTool;
134
135impl ReadFileTool {
136    pub fn new() -> Self {
137        Self
138    }
139}
140
141impl Default for ReadFileTool {
142    fn default() -> Self {
143        Self::new()
144    }
145}
146
147#[async_trait]
148impl Tool for ReadFileTool {
149    fn name(&self) -> &str {
150        "read_file"
151    }
152
153    fn description(&self) -> &str {
154        "Reads the contents of a file at the given path and returns the text."
155    }
156
157    fn parameters_schema(&self) -> Value {
158        json!({
159            "type": "object",
160            "properties": {
161                "path": {
162                    "type": "string",
163                    "description": "The filesystem path of the file to read."
164                }
165            },
166            "required": ["path"]
167        })
168    }
169
170    async fn execute(&self, args: Value) -> ToolResult {
171        let path = match args.get("path").and_then(|v| v.as_str()) {
172            Some(p) => p,
173            None => return ToolResult::err("Missing required parameter 'path'"),
174        };
175
176        match tokio::fs::read_to_string(path).await {
177            Ok(content) => ToolResult::ok_string(content),
178            Err(e) => ToolResult::err(format!("Failed to read file '{}': {}", path, e)),
179        }
180    }
181}
182
183// ---------------------------------------------------------------------------
184// WriteFileTool
185// ---------------------------------------------------------------------------
186
187/// Writes content to a file on disk.
188///
189/// Parameters:
190///   - `path`    (string, required): filesystem path to the file.
191///   - `content` (string, required): the text to write.
192#[derive(Debug)]
193pub struct WriteFileTool;
194
195impl WriteFileTool {
196    pub fn new() -> Self {
197        Self
198    }
199}
200
201impl Default for WriteFileTool {
202    fn default() -> Self {
203        Self::new()
204    }
205}
206
207#[async_trait]
208impl Tool for WriteFileTool {
209    fn name(&self) -> &str {
210        "write_file"
211    }
212
213    fn description(&self) -> &str {
214        "Writes the provided content to a file at the given path. Creates parent directories if needed. Overwrites existing files."
215    }
216
217    fn parameters_schema(&self) -> Value {
218        json!({
219            "type": "object",
220            "properties": {
221                "path": {
222                    "type": "string",
223                    "description": "The filesystem path of the file to write."
224                },
225                "content": {
226                    "type": "string",
227                    "description": "The content to write to the file."
228                }
229            },
230            "required": ["path", "content"]
231        })
232    }
233
234    async fn execute(&self, args: Value) -> ToolResult {
235        let path = match args.get("path").and_then(|v| v.as_str()) {
236            Some(p) => p.to_owned(),
237            None => return ToolResult::err("Missing required parameter 'path'"),
238        };
239        let content = match args.get("content").and_then(|v| v.as_str()) {
240            Some(c) => c.to_owned(),
241            None => return ToolResult::err("Missing required parameter 'content'"),
242        };
243
244        // Ensure parent directories exist.
245        if let Some(parent) = std::path::Path::new(&path).parent() {
246            if !parent.as_os_str().is_empty() {
247                if let Err(e) = tokio::fs::create_dir_all(parent).await {
248                    return ToolResult::err(format!(
249                        "Failed to create directories for '{}': {}",
250                        path, e
251                    ));
252                }
253            }
254        }
255
256        match tokio::fs::write(&path, &content).await {
257            Ok(()) => ToolResult::ok_string(format!("Successfully wrote to '{}'", path)),
258            Err(e) => ToolResult::err(format!("Failed to write file '{}': {}", path, e)),
259        }
260    }
261}
262
263// ---------------------------------------------------------------------------
264// AskUserTool
265// ---------------------------------------------------------------------------
266
267/// Signals that the agent needs to ask the user a question.
268///
269/// In practice the orchestrator will intercept this tool call and route the
270/// question to the user interface, then inject the user's reply as a tool
271/// result.  When executed directly it returns a placeholder response.
272///
273/// Parameters:
274///   - `question` (string, required): the question to ask the user.
275#[derive(Debug)]
276pub struct AskUserTool;
277
278impl AskUserTool {
279    pub fn new() -> Self {
280        Self
281    }
282}
283
284impl Default for AskUserTool {
285    fn default() -> Self {
286        Self::new()
287    }
288}
289
290#[async_trait]
291impl Tool for AskUserTool {
292    fn name(&self) -> &str {
293        "ask_user"
294    }
295
296    fn description(&self) -> &str {
297        "Ask the user a question. The orchestrator will route this to the user interface and the user's reply will be provided as the tool result."
298    }
299
300    fn parameters_schema(&self) -> Value {
301        json!({
302            "type": "object",
303            "properties": {
304                "question": {
305                    "type": "string",
306                    "description": "The question to ask the user."
307                }
308            },
309            "required": ["question"]
310        })
311    }
312
313    async fn execute(&self, args: Value) -> ToolResult {
314        let question = match args.get("question").and_then(|v| v.as_str()) {
315            Some(q) => q,
316            None => return ToolResult::err("Missing required parameter 'question'"),
317        };
318
319        // Placeholder: the orchestrator should intercept this tool call and
320        // replace the result with the actual user reply.
321        ToolResult::ok(json!({
322            "status": "pending_user_response",
323            "question": question,
324            "answer": null
325        }))
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    #[tokio::test]
334    async fn echo_works() {
335        let tool = EchoTool::new();
336        let result = tool.execute(json!({"message": "hello"})).await;
337        assert_eq!(result.status, super::super::ToolStatus::Success);
338        assert_eq!(result.data.as_str().unwrap(), "hello");
339    }
340
341    #[tokio::test]
342    async fn echo_missing_param() {
343        let tool = EchoTool::new();
344        let result = tool.execute(json!({})).await;
345        assert_eq!(result.status, super::super::ToolStatus::Error);
346    }
347
348    #[tokio::test]
349    async fn read_file_works() {
350        let dir = tempfile::tempdir().unwrap();
351        let path = dir.path().join("test.txt");
352        std::fs::write(&path, "hello world").unwrap();
353
354        let tool = ReadFileTool::new();
355        let result = tool.execute(json!({"path": path.to_str().unwrap()})).await;
356        assert_eq!(result.status, super::super::ToolStatus::Success);
357        assert_eq!(result.data.as_str().unwrap(), "hello world");
358    }
359
360    #[tokio::test]
361    async fn write_file_works() {
362        let dir = tempfile::tempdir().unwrap();
363        let path = dir.path().join("subdir").join("out.txt");
364
365        let tool = WriteFileTool::new();
366        let result = tool
367            .execute(json!({"path": path.to_str().unwrap(), "content": "data"}))
368            .await;
369        assert_eq!(result.status, super::super::ToolStatus::Success);
370        assert_eq!(std::fs::read_to_string(&path).unwrap(), "data");
371    }
372}