Skip to main content

ai_agent/tools/
bash.rs

1use crate::types::*;
2use std::process::Command;
3
4pub struct BashTool;
5
6impl BashTool {
7    pub fn new() -> Self {
8        Self
9    }
10
11    pub fn name(&self) -> &str {
12        "Bash"
13    }
14
15    pub fn description(&self) -> &str {
16        "Execute a shell command and return its output"
17    }
18
19    pub fn user_facing_name(&self, _input: Option<&serde_json::Value>) -> String {
20        "Bash".to_string()
21    }
22
23    pub fn get_tool_use_summary(&self, input: Option<&serde_json::Value>) -> Option<String> {
24        input.and_then(|inp| inp["command"].as_str().map(String::from))
25    }
26
27    pub fn render_tool_result_message(
28        &self,
29        content: &serde_json::Value,
30    ) -> Option<String> {
31        let content_str = content["content"].as_str()?;
32        if content_str.is_empty() {
33            Some("No output".to_string())
34        } else {
35            // Count lines in output
36            let line_count = content_str.lines().count();
37            Some(format!("{} {}", line_count, if line_count == 1 { "line" } else { "lines" }))
38        }
39    }
40
41    pub fn input_schema(&self) -> ToolInputSchema {
42        ToolInputSchema {
43            schema_type: "object".to_string(),
44            properties: serde_json::json!({
45                "command": { "type": "string", "description": "Shell command to execute" },
46                "description": { "type": "string", "description": "What this command does" }
47            }),
48            required: Some(vec!["command".to_string()]),
49        }
50    }
51
52    pub async fn execute(
53        &self,
54        input: serde_json::Value,
55        context: &ToolContext,
56    ) -> Result<ToolResult, crate::error::AgentError> {
57        let command = input["command"]
58            .as_str()
59            .ok_or_else(|| crate::error::AgentError::Tool("command required".to_string()))?
60            .to_string();
61
62        let cwd = context.cwd.clone();
63        let output = tokio::task::spawn_blocking(move || {
64            let mut cmd = Command::new("sh");
65            cmd.arg("-c").arg(&command);
66            if !cwd.is_empty() {
67                cmd.current_dir(&cwd);
68            }
69            cmd.output()
70        })
71        .await
72        .map_err(|e| crate::error::AgentError::Tool(e.to_string()))?
73        .map_err(|e| crate::error::AgentError::Tool(e.to_string()))?;
74
75        let stdout = String::from_utf8_lossy(&output.stdout);
76        let stderr = String::from_utf8_lossy(&output.stderr);
77
78        let content = if !stdout.is_empty() {
79            stdout.to_string()
80        } else {
81            stderr.to_string()
82        };
83
84        let is_error = !output.status.success();
85
86        Ok(ToolResult {
87            result_type: "tool_result".to_string(),
88            tool_use_id: "".to_string(),
89            content,
90            is_error: Some(is_error),
91            was_persisted: None,
92        })
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[tokio::test]
101    async fn test_bash_tool() {
102        let tool = BashTool::new();
103        let result = tool
104            .execute(
105                serde_json::json!({"command": "echo hello"}),
106                &ToolContext {
107                    cwd: "/tmp".to_string(),
108                    abort_signal: Default::default(),
109                },
110            )
111            .await;
112        assert!(result.is_ok());
113    }
114}