use std::path::PathBuf;
use std::pin::Pin;
use async_trait::async_trait;
use futures::Stream;
use serde::{Deserialize, Serialize};
use tokio_util::sync::CancellationToken;
use crate::config::{AgentKind, TaskConfig};
use crate::error::{Error, Result};
use crate::event::Event;
use crate::process::StreamHandle;
pub type EventStream = Pin<Box<dyn Stream<Item = Result<Event>> + Send>>;
pub fn find_binary(kind: AgentKind) -> Option<PathBuf> {
kind.binary_candidates()
.iter()
.find_map(|name| which::which(name).ok())
}
pub fn is_any_binary_available(kind: AgentKind) -> bool {
find_binary(kind).is_some()
}
pub fn resolve_binary(kind: AgentKind, config: &TaskConfig) -> Result<PathBuf> {
if let Some(ref p) = config.binary_path {
return Ok(p.clone());
}
find_binary(kind).ok_or_else(|| Error::BinaryNotFound {
agent: kind.display_name().to_string(),
binary: kind.binary_candidates().join(" or "),
})
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AgentCapabilities {
pub supports_system_prompt: bool,
pub supports_budget: bool,
pub supports_model: bool,
pub supports_max_turns: bool,
pub supports_append_system_prompt: bool,
}
#[derive(Debug, Clone)]
pub struct ConfigWarning {
pub message: String,
}
impl std::fmt::Display for ConfigWarning {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
#[async_trait]
pub trait AgentRunner: Send + Sync {
fn name(&self) -> &str;
fn is_available(&self) -> bool;
fn binary_path(&self, config: &TaskConfig) -> Result<std::path::PathBuf>;
fn build_args(&self, config: &TaskConfig) -> Vec<String>;
fn build_env(&self, config: &TaskConfig) -> Vec<(String, String)>;
async fn run(
&self,
config: &TaskConfig,
cancel_token: Option<CancellationToken>,
) -> Result<StreamHandle>;
fn version(&self, config: &TaskConfig) -> Option<String> {
let binary = self.binary_path(config).ok()?;
let output = std::process::Command::new(&binary)
.arg("--version")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.ok()?;
if output.status.success() {
let version_str = String::from_utf8_lossy(&output.stdout);
Some(version_str.trim().to_string())
} else {
None
}
}
fn capabilities(&self) -> AgentCapabilities {
AgentCapabilities::default()
}
fn validate_config(&self, config: &TaskConfig) -> Vec<ConfigWarning> {
let caps = self.capabilities();
let mut warnings = Vec::new();
if config.system_prompt.is_some() && !caps.supports_system_prompt {
warnings.push(ConfigWarning {
message: format!("{} does not support --system-prompt", self.name()),
});
}
if config.max_budget_usd.is_some() && !caps.supports_budget {
warnings.push(ConfigWarning {
message: format!("{} does not support --max-budget", self.name()),
});
}
if config.model.is_some() && !caps.supports_model {
warnings.push(ConfigWarning {
message: format!("{} does not support --model", self.name()),
});
}
if config.max_turns.is_some() && !caps.supports_max_turns {
warnings.push(ConfigWarning {
message: format!("{} does not support --max-turns", self.name()),
});
}
if config.append_system_prompt.is_some() && !caps.supports_append_system_prompt {
warnings.push(ConfigWarning {
message: format!("{} does not support --append-system-prompt", self.name()),
});
}
warnings
}
}