1use std::path::PathBuf;
2use std::pin::Pin;
3
4use async_trait::async_trait;
5use futures::Stream;
6use serde::{Deserialize, Serialize};
7use tokio_util::sync::CancellationToken;
8
9use crate::config::{AgentKind, TaskConfig};
10use crate::error::{Error, Result};
11use crate::event::Event;
12use crate::process::StreamHandle;
13
14pub type EventStream = Pin<Box<dyn Stream<Item = Result<Event>> + Send>>;
16
17pub fn find_binary(kind: AgentKind) -> Option<PathBuf> {
19 kind.binary_candidates()
20 .iter()
21 .find_map(|name| which::which(name).ok())
22}
23
24pub fn is_any_binary_available(kind: AgentKind) -> bool {
26 find_binary(kind).is_some()
27}
28
29pub fn resolve_binary(kind: AgentKind, config: &TaskConfig) -> Result<PathBuf> {
31 if let Some(ref p) = config.binary_path {
32 return Ok(p.clone());
33 }
34 find_binary(kind).ok_or_else(|| Error::BinaryNotFound {
35 agent: kind.display_name().to_string(),
36 binary: kind.binary_candidates().join(" or "),
37 })
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, Default)]
42pub struct AgentCapabilities {
43 pub supports_system_prompt: bool,
44 pub supports_budget: bool,
45 pub supports_model: bool,
46 pub supports_max_turns: bool,
47 pub supports_append_system_prompt: bool,
48}
49
50#[derive(Debug, Clone)]
52pub struct ConfigWarning {
53 pub message: String,
54}
55
56impl std::fmt::Display for ConfigWarning {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 write!(f, "{}", self.message)
59 }
60}
61
62#[async_trait]
68pub trait AgentRunner: Send + Sync {
69 fn name(&self) -> &str;
71
72 fn is_available(&self) -> bool;
74
75 fn binary_path(&self, config: &TaskConfig) -> Result<std::path::PathBuf>;
77
78 fn build_args(&self, config: &TaskConfig) -> Vec<String>;
80
81 fn build_env(&self, config: &TaskConfig) -> Vec<(String, String)>;
83
84 async fn run(
86 &self,
87 config: &TaskConfig,
88 cancel_token: Option<CancellationToken>,
89 ) -> Result<StreamHandle>;
90
91 fn version(&self, config: &TaskConfig) -> Option<String> {
93 let binary = self.binary_path(config).ok()?;
94 let output = std::process::Command::new(&binary)
95 .arg("--version")
96 .stdout(std::process::Stdio::piped())
97 .stderr(std::process::Stdio::piped())
98 .output()
99 .ok()?;
100
101 if output.status.success() {
102 let version_str = String::from_utf8_lossy(&output.stdout);
103 Some(version_str.trim().to_string())
104 } else {
105 None
106 }
107 }
108
109 fn capabilities(&self) -> AgentCapabilities {
111 AgentCapabilities::default()
113 }
114
115 fn validate_config(&self, config: &TaskConfig) -> Vec<ConfigWarning> {
117 let caps = self.capabilities();
118 let mut warnings = Vec::new();
119
120 if config.system_prompt.is_some() && !caps.supports_system_prompt {
121 warnings.push(ConfigWarning {
122 message: format!("{} does not support --system-prompt", self.name()),
123 });
124 }
125 if config.max_budget_usd.is_some() && !caps.supports_budget {
126 warnings.push(ConfigWarning {
127 message: format!("{} does not support --max-budget", self.name()),
128 });
129 }
130 if config.model.is_some() && !caps.supports_model {
131 warnings.push(ConfigWarning {
132 message: format!("{} does not support --model", self.name()),
133 });
134 }
135 if config.max_turns.is_some() && !caps.supports_max_turns {
136 warnings.push(ConfigWarning {
137 message: format!("{} does not support --max-turns", self.name()),
138 });
139 }
140 if config.append_system_prompt.is_some() && !caps.supports_append_system_prompt {
141 warnings.push(ConfigWarning {
142 message: format!("{} does not support --append-system-prompt", self.name()),
143 });
144 }
145
146 warnings
147 }
148}