Skip to main content

chant/
tools.rs

1//! Tool definitions for ollama-rs function calling integration.
2//!
3//! These tools provide the agent with the ability to read files, write files,
4//! run commands, and list files during spec execution.
5
6use anyhow::Result;
7use serde_json::{json, Value};
8use std::fs;
9use std::process::Command;
10
11/// Get JSON schema definitions for all available tools.
12/// These schemas are passed to the ollama model to enable function calling.
13pub fn get_tool_definitions() -> Vec<Value> {
14    vec![
15        json!({
16            "type": "function",
17            "function": {
18                "name": "read_file",
19                "description": "Read the contents of a file at the given path",
20                "parameters": {
21                    "type": "object",
22                    "properties": {
23                        "path": {
24                            "type": "string",
25                            "description": "File path to read"
26                        }
27                    },
28                    "required": ["path"]
29                }
30            }
31        }),
32        json!({
33            "type": "function",
34            "function": {
35                "name": "write_file",
36                "description": "Write content to a file, creating or overwriting it",
37                "parameters": {
38                    "type": "object",
39                    "properties": {
40                        "path": {
41                            "type": "string",
42                            "description": "File path to write to"
43                        },
44                        "content": {
45                            "type": "string",
46                            "description": "Content to write to the file"
47                        }
48                    },
49                    "required": ["path", "content"]
50                }
51            }
52        }),
53        json!({
54            "type": "function",
55            "function": {
56                "name": "run_command",
57                "description": "Run a shell command and return its output",
58                "parameters": {
59                    "type": "object",
60                    "properties": {
61                        "command": {
62                            "type": "string",
63                            "description": "Shell command to execute"
64                        }
65                    },
66                    "required": ["command"]
67                }
68            }
69        }),
70        json!({
71            "type": "function",
72            "function": {
73                "name": "list_files",
74                "description": "List files matching a glob pattern",
75                "parameters": {
76                    "type": "object",
77                    "properties": {
78                        "pattern": {
79                            "type": "string",
80                            "description": "Glob pattern like 'src/**/*.rs' or '*.txt'"
81                        }
82                    },
83                    "required": ["pattern"]
84                }
85            }
86        }),
87        json!({
88            "type": "function",
89            "function": {
90                "name": "task_complete",
91                "description": "Signal that the task has been completed successfully",
92                "parameters": {
93                    "type": "object",
94                    "properties": {
95                        "summary": {
96                            "type": "string",
97                            "description": "Brief summary of what was accomplished"
98                        }
99                    },
100                    "required": ["summary"]
101                }
102            }
103        }),
104    ]
105}
106
107/// Execute a tool by name with the given arguments.
108/// Returns the result as a string to be sent back to the model.
109pub fn execute_tool(name: &str, args: &Value) -> Result<String, String> {
110    match name {
111        "read_file" => {
112            let path = args["path"]
113                .as_str()
114                .ok_or_else(|| "missing or invalid 'path' parameter".to_string())?;
115            read_file(path.to_string()).map_err(|e| format!("read_file failed: {}", e))
116        }
117        "write_file" => {
118            let path = args["path"]
119                .as_str()
120                .ok_or_else(|| "missing or invalid 'path' parameter".to_string())?;
121            let content = args["content"]
122                .as_str()
123                .ok_or_else(|| "missing or invalid 'content' parameter".to_string())?;
124            write_file(path.to_string(), content.to_string())
125                .map_err(|e| format!("write_file failed: {}", e))
126        }
127        "run_command" => {
128            let command = args["command"]
129                .as_str()
130                .ok_or_else(|| "missing or invalid 'command' parameter".to_string())?;
131            run_command(command.to_string()).map_err(|e| format!("run_command failed: {}", e))
132        }
133        "list_files" => {
134            let pattern = args["pattern"]
135                .as_str()
136                .ok_or_else(|| "missing or invalid 'pattern' parameter".to_string())?;
137            list_files(pattern.to_string()).map_err(|e| format!("list_files failed: {}", e))
138        }
139        "task_complete" => {
140            let summary = args["summary"].as_str().unwrap_or("Task completed");
141            Ok(format!("TASK_COMPLETE: {}", summary))
142        }
143        _ => Err(format!("Unknown tool: {}", name)),
144    }
145}
146
147/// Read the contents of a file at the given path.
148/// Use this to understand existing code before making changes.
149pub fn read_file(path: String) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
150    Ok(fs::read_to_string(&path)?)
151}
152
153/// Write content to a file at the given path.
154/// Creates the file if it doesn't exist, overwrites if it does.
155pub fn write_file(
156    path: String,
157    content: String,
158) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
159    fs::write(&path, &content)?;
160    Ok(format!("Wrote {} bytes to {}", content.len(), path))
161}
162
163/// Run a shell command and return its output.
164/// Use for: git operations, cargo build/test, file operations.
165pub fn run_command(command: String) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
166    let output = Command::new("sh").arg("-c").arg(&command).output()?;
167
168    let stdout = String::from_utf8_lossy(&output.stdout);
169    let stderr = String::from_utf8_lossy(&output.stderr);
170
171    if output.status.success() {
172        Ok(stdout.to_string())
173    } else {
174        Ok(format!(
175            "Command failed:\nstdout: {}\nstderr: {}",
176            stdout, stderr
177        ))
178    }
179}
180
181/// List files matching a glob pattern.
182/// Example: list_files("src/**/*.rs") returns all Rust files in src/
183pub fn list_files(pattern: String) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
184    use glob::glob;
185
186    let paths: Vec<_> = glob(&pattern)?
187        .filter_map(Result::ok)
188        .map(|p| p.display().to_string())
189        .collect();
190    Ok(paths.join("\n"))
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use std::fs;
197    use tempfile::tempdir;
198
199    #[test]
200    fn test_write_file() {
201        let dir = tempdir().unwrap();
202        let file_path = dir.path().join("test.txt");
203        let path_str = file_path.to_string_lossy().to_string();
204
205        let result = write_file(path_str.clone(), "test content".to_string()).unwrap();
206        assert!(result.contains("12 bytes"));
207
208        let content = fs::read_to_string(&file_path).unwrap();
209        assert_eq!(content, "test content");
210    }
211
212    #[test]
213    fn test_read_file() {
214        let dir = tempdir().unwrap();
215        let file_path = dir.path().join("test.txt");
216        fs::write(&file_path, "test content").unwrap();
217
218        let path_str = file_path.to_string_lossy().to_string();
219        let content = read_file(path_str).unwrap();
220        assert_eq!(content, "test content");
221    }
222
223    #[test]
224    fn test_run_command() {
225        let result = run_command("echo 'test output'".to_string()).unwrap();
226        assert!(result.contains("test output"));
227    }
228
229    #[test]
230    fn test_list_files() {
231        let dir = tempdir().unwrap();
232        let _file1 = fs::File::create(dir.path().join("file1.txt")).unwrap();
233        let _file2 = fs::File::create(dir.path().join("file2.txt")).unwrap();
234
235        let pattern = format!("{}/*.txt", dir.path().display());
236        let result = list_files(pattern).unwrap();
237
238        assert!(result.contains("file1.txt"));
239        assert!(result.contains("file2.txt"));
240    }
241}