use car_inference::adaptive_router::TaskComplexity;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EngineChoice {
Auto,
Native,
External(String),
Foreman(String),
}
impl EngineChoice {
pub fn parse(s: &str) -> Result<Self, String> {
match s.trim() {
"auto" | "" => Ok(Self::Auto),
"native" => Ok(Self::Native),
"external" => Ok(Self::External(String::new())),
"foreman" => Ok(Self::Foreman(String::new())),
other => {
if let Some(id) = other.strip_prefix("external:") {
if !id.is_empty() {
return Ok(Self::External(id.to_string()));
}
}
if let Some(id) = other.strip_prefix("foreman:") {
if !id.is_empty() {
return Ok(Self::Foreman(id.to_string()));
}
}
Err(format!(
"unknown engine '{other}' (expected auto | native | external[:agent_id] | foreman[:agent_id])"
))
}
}
}
pub fn label(&self) -> String {
match self {
Self::Auto => "auto".to_string(),
Self::Native => "native".to_string(),
Self::External(id) if id.is_empty() => "external".to_string(),
Self::External(id) => format!("external:{id}"),
Self::Foreman(id) if id.is_empty() => "foreman".to_string(),
Self::Foreman(id) => format!("foreman:{id}"),
}
}
}
#[derive(Debug, Clone)]
pub struct DetectedAgent {
pub id: String,
pub ready: bool,
pub unusable_reason: Option<String>,
}
impl DetectedAgent {
fn unavailable_reason(&self, kind: &str) -> Option<String> {
if let Some(why) = &self.unusable_reason {
return Some(format!(
"{kind} '{}' is installed but cannot be executed: {why}",
self.id
));
}
if !self.ready {
return Some(format!(
"{kind} '{}' is installed but not ready (not authenticated?)",
self.id
));
}
None
}
}
pub const DEFAULT_PREFERENCE: [&str; 3] = ["claude-code", "codex", "gemini"];
#[derive(Debug, Clone, PartialEq)]
pub struct ResolvedEngine {
pub engine: EngineChoice,
pub reason: String,
}
pub async fn detect_ready_agents() -> Vec<DetectedAgent> {
car_external_agents::detect_with_health(false)
.await
.into_iter()
.map(|spec| DetectedAgent {
ready: matches!(
&spec.health,
Some(h) if h.status == car_external_agents::HealthStatus::Ready
),
unusable_reason: spec.unusable_reason().map(str::to_string),
id: spec.id,
})
.collect()
}
fn first_ready(preference: &[&str], detected: &[DetectedAgent]) -> Option<String> {
preference
.iter()
.find(|p| detected.iter().any(|d| d.ready && d.id == **p))
.map(|p| p.to_string())
.or_else(|| detected.iter().find(|d| d.ready).map(|d| d.id.clone()))
}
pub fn resolve_engine(
requested: &EngineChoice,
intent: &str,
detected: &[DetectedAgent],
preference: &[&str],
) -> Result<ResolvedEngine, String> {
match requested {
EngineChoice::Native => Ok(ResolvedEngine {
engine: EngineChoice::Native,
reason: "explicitly requested".into(),
}),
EngineChoice::External(id) if !id.is_empty() => {
let agent = detected
.iter()
.find(|d| d.id == *id)
.ok_or_else(|| format!("external agent '{id}' is not installed"))?;
if let Some(why) = agent.unavailable_reason("external agent") {
return Err(why);
}
Ok(ResolvedEngine {
engine: EngineChoice::External(id.clone()),
reason: "explicitly requested".into(),
})
}
EngineChoice::External(_) => {
let id = first_ready(preference, detected).ok_or(
"external engine requested but no external agent CLI is installed and ready",
)?;
Ok(ResolvedEngine {
engine: EngineChoice::External(id),
reason: "first ready external agent".into(),
})
}
EngineChoice::Foreman(id) if !id.is_empty() => {
let agent = detected
.iter()
.find(|d| d.id == *id)
.ok_or_else(|| format!("foreman adapter '{id}' is not installed"))?;
if let Some(why) = agent.unavailable_reason("foreman adapter") {
return Err(why);
}
Ok(ResolvedEngine {
engine: EngineChoice::Foreman(id.clone()),
reason: "explicitly requested".into(),
})
}
EngineChoice::Foreman(_) => {
let id = first_ready(preference, detected).ok_or(
"foreman engine requested but no external agent CLI is installed and ready",
)?;
Ok(ResolvedEngine {
engine: EngineChoice::Foreman(id),
reason: "first ready external agent".into(),
})
}
EngineChoice::Auto => {
let complexity = TaskComplexity::assess(intent);
let broad_scope = intent.split_whitespace().count() > 120;
let frontier_worthy = complexity == TaskComplexity::Complex
|| (complexity == TaskComplexity::Code && broad_scope);
if frontier_worthy {
if let Some(id) = first_ready(preference, detected) {
return Ok(ResolvedEngine {
engine: EngineChoice::Foreman(id),
reason: format!(
"task assessed as {complexity:?} with broad scope and a frontier CLI is ready; \
foreman gates the parallel farm-out"
),
});
}
}
Ok(ResolvedEngine {
engine: EngineChoice::Native,
reason: if frontier_worthy {
"task is complex but no external CLI is ready; using native loop".into()
} else {
format!("task assessed as {complexity:?}; native loop suffices")
},
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn agents(ready: &[&str], installed_not_ready: &[&str]) -> Vec<DetectedAgent> {
ready
.iter()
.map(|id| DetectedAgent {
id: id.to_string(),
ready: true,
unusable_reason: None,
})
.chain(installed_not_ready.iter().map(|id| DetectedAgent {
id: id.to_string(),
ready: false,
unusable_reason: None,
}))
.collect()
}
#[test]
fn explicit_engine_reports_unrunnable_over_auth() {
let detected = vec![DetectedAgent {
id: "codex".to_string(),
ready: false,
unusable_reason: Some("killed by signal 9".to_string()),
}];
let err = resolve_engine(
&EngineChoice::External("codex".to_string()),
"add a test",
&detected,
&DEFAULT_PREFERENCE,
)
.unwrap_err();
assert!(
err.contains("cannot be executed") && err.contains("signal 9"),
"want the executability diagnosis, got {err:?}"
);
assert!(
!err.contains("not authenticated"),
"must not blame auth for an unrunnable binary, got {err:?}"
);
}
fn complex_intent() -> String {
format!(
"Refactor the authentication architecture across the whole system, design and \
implement the migration step by step, then analyze the tradeoffs. {}",
"Consider every module and integration in depth. ".repeat(30)
)
}
#[test]
fn explicit_choice_always_wins() {
let detected = agents(&["claude-code"], &[]);
let r = resolve_engine(
&EngineChoice::Native,
&complex_intent(),
&detected,
&DEFAULT_PREFERENCE,
)
.unwrap();
assert_eq!(r.engine, EngineChoice::Native);
let r = resolve_engine(
&EngineChoice::External("claude-code".into()),
"tiny task",
&detected,
&DEFAULT_PREFERENCE,
)
.unwrap();
assert_eq!(r.engine, EngineChoice::External("claude-code".into()));
}
#[test]
fn explicit_external_fails_clearly_when_unavailable() {
let err = resolve_engine(
&EngineChoice::External("codex".into()),
"x",
&agents(&[], &["codex"]),
&DEFAULT_PREFERENCE,
)
.unwrap_err();
assert!(err.contains("not ready"), "{err}");
let err = resolve_engine(
&EngineChoice::External("codex".into()),
"x",
&agents(&[], &[]),
&DEFAULT_PREFERENCE,
)
.unwrap_err();
assert!(err.contains("not installed"), "{err}");
}
#[test]
fn auto_with_no_clis_is_native() {
let r = resolve_engine(
&EngineChoice::Auto,
&complex_intent(),
&[],
&DEFAULT_PREFERENCE,
)
.unwrap();
assert_eq!(r.engine, EngineChoice::Native);
assert!(r.reason.contains("no external CLI"), "{}", r.reason);
}
#[test]
fn auto_simple_task_stays_native_even_with_clis() {
let detected = agents(&["claude-code"], &[]);
let r = resolve_engine(
&EngineChoice::Auto,
"fix typo in README",
&detected,
&DEFAULT_PREFERENCE,
)
.unwrap();
assert_eq!(r.engine, EngineChoice::Native, "{}", r.reason);
}
#[test]
fn auto_complex_task_delegates_foreman_first_in_preference_order() {
let detected = agents(&["gemini", "claude-code"], &["codex"]);
let r = resolve_engine(
&EngineChoice::Auto,
&complex_intent(),
&detected,
&DEFAULT_PREFERENCE,
)
.unwrap();
assert_eq!(
r.engine,
EngineChoice::Foreman("claude-code".into()),
"foreman-first with preference order: {}",
r.reason
);
}
#[test]
fn explicit_foreman_resolves_and_fails_clearly() {
let detected = agents(&["codex"], &["claude-code"]);
let r = resolve_engine(
&EngineChoice::Foreman(String::new()),
"x",
&detected,
&DEFAULT_PREFERENCE,
)
.unwrap();
assert_eq!(r.engine, EngineChoice::Foreman("codex".into()));
let err = resolve_engine(
&EngineChoice::Foreman("claude-code".into()),
"x",
&detected,
&DEFAULT_PREFERENCE,
)
.unwrap_err();
assert!(err.contains("not ready"), "{err}");
let err = resolve_engine(
&EngineChoice::Foreman(String::new()),
"x",
&agents(&[], &[]),
&DEFAULT_PREFERENCE,
)
.unwrap_err();
assert!(err.contains("no external agent"), "{err}");
}
#[test]
fn ready_agent_outside_preference_still_selected() {
let detected = agents(&["future-cli"], &[]);
let r = resolve_engine(
&EngineChoice::External(String::new()),
"x",
&detected,
&DEFAULT_PREFERENCE,
)
.unwrap();
assert_eq!(r.engine, EngineChoice::External("future-cli".into()));
}
#[test]
fn parse_round_trips() {
for (input, expect) in [
("auto", EngineChoice::Auto),
("", EngineChoice::Auto),
("native", EngineChoice::Native),
("external", EngineChoice::External(String::new())),
(
"external:claude-code",
EngineChoice::External("claude-code".into()),
),
("foreman", EngineChoice::Foreman(String::new())),
("foreman:codex", EngineChoice::Foreman("codex".into())),
] {
assert_eq!(EngineChoice::parse(input).unwrap(), expect);
}
assert!(EngineChoice::parse("warp-drive").is_err());
assert!(EngineChoice::parse("external:").is_err());
assert!(EngineChoice::parse("foreman:").is_err());
}
}