An AI agent built to do Ralph loops - plan mode for planning and ralph mode for implementing.

feat: implement shell command execution tool

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

+106
+1
src/tools/mod.rs
··· 69 69 } 70 70 71 71 pub mod file; 72 + pub mod shell;
+81
src/tools/shell.rs
··· 1 + use async_trait::async_trait; 2 + use anyhow::{Context, Result}; 3 + use serde_json::json; 4 + use tokio::process::Command; 5 + 6 + use crate::tools::Tool; 7 + 8 + /// Tool for executing shell commands 9 + pub struct RunCommandTool; 10 + 11 + #[async_trait] 12 + impl Tool for RunCommandTool { 13 + fn name(&self) -> &str { 14 + "run_command" 15 + } 16 + 17 + fn description(&self) -> &str { 18 + "Execute a shell command and return its output. Supports optional working directory." 19 + } 20 + 21 + fn parameters(&self) -> serde_json::Value { 22 + json!({ 23 + "type": "object", 24 + "properties": { 25 + "command": { 26 + "type": "string", 27 + "description": "The shell command to execute" 28 + }, 29 + "working_dir": { 30 + "type": "string", 31 + "description": "Optional working directory for command execution" 32 + } 33 + }, 34 + "required": ["command"] 35 + }) 36 + } 37 + 38 + async fn execute(&self, params: serde_json::Value) -> Result<String> { 39 + let command = params["command"] 40 + .as_str() 41 + .context("command parameter is required")?; 42 + 43 + let working_dir = params["working_dir"].as_str(); 44 + 45 + // Use sh -c on Unix, cmd /C on Windows 46 + #[cfg(unix)] 47 + let (shell, shell_arg) = ("sh", "-c"); 48 + 49 + #[cfg(windows)] 50 + let (shell, shell_arg) = ("cmd", "/C"); 51 + 52 + let mut cmd = Command::new(shell); 53 + cmd.arg(shell_arg).arg(command); 54 + 55 + if let Some(dir) = working_dir { 56 + cmd.current_dir(dir); 57 + } 58 + 59 + let output = cmd 60 + .output() 61 + .await 62 + .context("Failed to execute command")?; 63 + 64 + let stdout = String::from_utf8_lossy(&output.stdout); 65 + let stderr = String::from_utf8_lossy(&output.stderr); 66 + 67 + if output.status.success() { 68 + if stderr.is_empty() { 69 + Ok(stdout.to_string()) 70 + } else { 71 + Ok(format!("stdout:\n{}\n\nstderr:\n{}", stdout, stderr)) 72 + } 73 + } else { 74 + let exit_code = output.status.code().unwrap_or(-1); 75 + Ok(format!( 76 + "Command failed with exit code {}:\nstdout:\n{}\nstderr:\n{}", 77 + exit_code, stdout, stderr 78 + )) 79 + } 80 + } 81 + }
+24
tests/tools_test.rs
··· 43 43 } 44 44 45 45 use rustagent::tools::file::{ReadFileTool, WriteFileTool, ListFilesTool}; 46 + use rustagent::tools::shell::RunCommandTool; 46 47 use serde_json::json; 47 48 use tempfile::TempDir; 48 49 use std::fs; ··· 90 91 assert!(result.contains("file1.txt")); 91 92 assert!(result.contains("file2.txt")); 92 93 } 94 + 95 + #[tokio::test] 96 + async fn test_run_command_tool() { 97 + let tool = RunCommandTool; 98 + let result = tool.execute(json!({ 99 + "command": "echo hello" 100 + })).await.unwrap(); 101 + 102 + assert!(result.contains("hello")); 103 + } 104 + 105 + #[tokio::test] 106 + async fn test_run_command_with_working_dir() { 107 + let temp = TempDir::new().unwrap(); 108 + 109 + let tool = RunCommandTool; 110 + let result = tool.execute(json!({ 111 + "command": "pwd", 112 + "working_dir": temp.path().to_str().unwrap() 113 + })).await.unwrap(); 114 + 115 + assert!(result.contains(temp.path().to_str().unwrap())); 116 + }