cascade_agent/tools/
builtin.rs1use async_trait::async_trait;
4use serde_json::{json, Value};
5
6use super::{Tool, ToolResult};
7
8#[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#[derive(Debug)]
71pub struct ListToolsTool {
72 tools: Vec<(String, String)>,
73}
74
75impl ListToolsTool {
76 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 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#[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#[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 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#[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 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}