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 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
),
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 !agent.ready {
return Err(format!(
"external agent '{id}' is installed but not ready (not authenticated?)"
));
}
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 !agent.ready {
return Err(format!(
"foreman adapter '{id}' is installed but not ready (not authenticated?)"
));
}
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 })
.chain(installed_not_ready.iter().map(|id| DetectedAgent {
id: id.to_string(),
ready: false,
}))
.collect()
}
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());
}
}