use std::collections::BTreeMap;
use std::io::{BufRead, BufReader, Write};
use std::process::{Command, Stdio};
use std::time::Instant;
use crate::error::{Result, SkadooshError};
pub trait ToolExecutor: Send + Sync {
fn execute(&self, name: &str, arguments: &str) -> Result<String>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct ShellExecutor;
impl ShellExecutor {
pub fn new() -> Self {
Self
}
pub async fn execute_streaming(
name: &str,
arguments: &str,
mut on_line: impl FnMut(String),
) -> Result<(String, std::time::Duration)> {
let started = Instant::now();
let mut child = Command::new(name)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| SkadooshError::Other(anyhow::anyhow!("spawn '{name}': {e}")))?;
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(arguments.as_bytes());
}
let mut full_output = String::new();
if let Some(stdout) = child.stdout.take() {
let reader = BufReader::new(stdout);
for line in reader.lines() {
match line {
Ok(l) => {
full_output.push_str(&l);
full_output.push('\n');
on_line(l);
}
Err(e) => {
tracing::warn!(tool=%name, error=%e, "failed reading tool stdout line");
break;
}
}
}
}
let output = child
.wait_with_output()
.map_err(|e| SkadooshError::Other(anyhow::anyhow!("wait '{name}': {e}")))?;
let elapsed = started.elapsed();
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(SkadooshError::Other(anyhow::anyhow!(
"'{name}' exited {}: {}",
output.status,
stderr.trim()
)));
}
if full_output.is_empty() {
full_output = String::from_utf8_lossy(&output.stdout).into_owned();
}
Ok((full_output, elapsed))
}
}
impl ToolExecutor for ShellExecutor {
fn execute(&self, name: &str, arguments: &str) -> Result<String> {
let mut child = Command::new(name)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| SkadooshError::Other(anyhow::anyhow!("spawn '{name}': {e}")))?;
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(arguments.as_bytes());
}
let output = child
.wait_with_output()
.map_err(|e| SkadooshError::Other(anyhow::anyhow!("wait '{name}': {e}")))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(SkadooshError::Other(anyhow::anyhow!(
"'{name}' exited {}: {}",
output.status,
stderr.trim()
)));
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopExecutor;
impl NoopExecutor {
pub fn new() -> Self {
Self
}
}
impl ToolExecutor for NoopExecutor {
fn execute(&self, _name: &str, _arguments: &str) -> Result<String> {
Ok("{\"error\":\"tool execution not configured; respond with text\"}".to_string())
}
}
pub async fn execute_parallel(
calls: Vec<(String, String, String)>, ) -> BTreeMap<String, std::result::Result<String, SkadooshError>> {
let mut handles = Vec::new();
for (name, args, call_id) in calls {
let handle = tokio::spawn(async move {
let exec = ShellExecutor::new();
let result = exec.execute(&name, &args);
(call_id, result)
});
handles.push(handle);
}
let mut results = BTreeMap::new();
for handle in handles {
match handle.await {
Ok((call_id, result)) => {
results.insert(call_id, result);
}
Err(join_err) => {
tracing::error!(error=%join_err, "tool execution task panicked");
}
}
}
results
}