llm_coding_tools_rig/
bash.rs1use llm_coding_tools_core::operations::execute_command;
6use llm_coding_tools_core::tool_names;
7use llm_coding_tools_core::{ToolContext, ToolError, ToolOutput};
8use rig::completion::ToolDefinition;
9use rig::tool::Tool;
10use schemars::{schema_for, JsonSchema};
11use serde::Deserialize;
12use std::path::Path;
13use std::time::Duration;
14
15const DEFAULT_TIMEOUT_MS: u64 = 120_000;
17
18fn default_timeout_ms() -> u64 {
19 DEFAULT_TIMEOUT_MS
20}
21
22#[derive(Debug, Clone, Deserialize, JsonSchema)]
24pub struct BashArgs {
25 pub command: String,
27 pub workdir: Option<String>,
29 #[serde(default = "default_timeout_ms")]
31 pub timeout_ms: u64,
32}
33
34#[derive(Debug, Clone, Copy, Default)]
38pub struct BashTool;
39
40impl BashTool {
41 #[inline]
43 pub fn new() -> Self {
44 Self
45 }
46}
47
48impl Tool for BashTool {
49 const NAME: &'static str = tool_names::BASH;
50
51 type Error = ToolError;
52 type Args = BashArgs;
53 type Output = ToolOutput;
54
55 async fn definition(&self, _prompt: String) -> ToolDefinition {
56 ToolDefinition {
57 name: <Self as Tool>::NAME.to_string(),
58 description: "Execute a shell command with optional working directory and timeout."
59 .to_string(),
60 parameters: serde_json::to_value(schema_for!(BashArgs))
61 .expect("schema serialization should never fail"),
62 }
63 }
64
65 async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
66 let workdir = args.workdir.as_ref().map(Path::new);
67 let timeout = Duration::from_millis(args.timeout_ms);
68
69 let result = execute_command(&args.command, workdir, timeout).await?;
70 Ok(result.format_output())
71 }
72}
73
74impl ToolContext for BashTool {
75 const NAME: &'static str = tool_names::BASH;
76
77 fn context(&self) -> &'static str {
78 llm_coding_tools_core::context::BASH
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[tokio::test]
87 async fn executes_echo() {
88 let tool = BashTool::new();
89 let args = BashArgs {
90 command: "echo hello".to_string(),
91 workdir: None,
92 timeout_ms: 5000,
93 };
94 let result = tool.call(args).await.unwrap();
95 assert!(result.content.contains("hello"));
96 }
97
98 #[tokio::test]
99 async fn timeout_returns_error() {
100 let tool = BashTool::new();
101 let cmd = if cfg!(target_os = "windows") {
102 "ping -n 10 127.0.0.1"
103 } else {
104 "sleep 10"
105 };
106 let args = BashArgs {
107 command: cmd.to_string(),
108 workdir: None,
109 timeout_ms: 100,
110 };
111 let result = tool.call(args).await;
112 assert!(matches!(result, Err(ToolError::Timeout(_))));
113 }
114}