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 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}