use anyhow::{Context, Result};
use std::path::Path;
use std::process::{Command, Stdio};
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct ToolOutput {
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
pub exit_code: Option<i32>,
}
impl ToolOutput {
pub fn success(&self) -> bool {
self.exit_code.map_or(false, |code| code == 0)
}
pub fn stdout_str(&self) -> Result<&str, std::str::Utf8Error> {
std::str::from_utf8(&self.stdout)
}
pub fn stderr_str(&self) -> Result<&str, std::str::Utf8Error> {
std::str::from_utf8(&self.stderr)
}
}
#[derive(Debug, thiserror::Error)]
pub enum ToolInvocationError {
#[error("Tool not found: {path}")]
ToolNotFound { path: String },
#[error("Tool execution failed: {tool}")]
ExecutionFailed { tool: String, reason: String },
#[error("Timeout: {tool} did not complete within {timeout_secs}s")]
Timeout { tool: String, timeout_secs: u64 },
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
pub fn invoke_tool_with_timeout(
executable: &Path,
args: &[&str],
_timeout: Duration,
) -> Result<ToolOutput, ToolInvocationError> {
if !executable.exists() {
return Err(ToolInvocationError::ToolNotFound {
path: executable.display().to_string(),
});
}
let tool_name = executable
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("tool")
.to_string();
let output = Command::new(executable).args(args).output().map_err(|e| {
ToolInvocationError::ExecutionFailed {
tool: tool_name.clone(),
reason: e.to_string(),
}
})?;
Ok(ToolOutput {
stdout: output.stdout,
stderr: output.stderr,
exit_code: output.status.code(),
})
}
pub fn invoke_tool_sync(
executable: &Path,
args: &[&str],
) -> Result<ToolOutput, ToolInvocationError> {
if !executable.exists() {
return Err(ToolInvocationError::ToolNotFound {
path: executable.display().to_string(),
});
}
let tool_name = executable
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("tool")
.to_string();
let output = Command::new(executable).args(args).output().map_err(|e| {
ToolInvocationError::ExecutionFailed {
tool: tool_name.clone(),
reason: e.to_string(),
}
})?;
Ok(ToolOutput {
stdout: output.stdout,
stderr: output.stderr,
exit_code: output.status.code(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tool_output_success() {
let output = ToolOutput {
stdout: b"hello".to_vec(),
stderr: b"".to_vec(),
exit_code: Some(0),
};
assert!(output.success());
assert_eq!(output.stdout_str().unwrap(), "hello");
}
#[test]
fn test_tool_output_failure() {
let output = ToolOutput {
stdout: b"".to_vec(),
stderr: b"error".to_vec(),
exit_code: Some(1),
};
assert!(!output.success());
assert_eq!(output.stderr_str().unwrap(), "error");
}
#[test]
fn test_invoke_tool_sync_not_found() {
let result = invoke_tool_sync(Path::new("/nonexistent/tool"), &[]);
assert!(matches!(
result,
Err(ToolInvocationError::ToolNotFound { .. })
));
}
#[cfg(unix)]
#[test]
fn test_invoke_tool_sync_echo() {
let result = invoke_tool_sync(Path::new("/bin/echo"), &["hello"]);
assert!(result.is_ok());
let output = result.unwrap();
assert!(output.success());
assert_eq!(output.stdout_str().unwrap().trim(), "hello");
}
}