car-server-core 0.24.1

Transport-neutral library for the CAR daemon JSON-RPC dispatcher (used by car-server and tokhn-daemon)
//! Engine selection: native CAR loop vs. delegation to an external CLI.
//!
//! Resolution order: an explicit choice always wins; `Auto` assesses the
//! intent with `car-inference`'s [`TaskComplexity`] heuristics and delegates
//! to the preferred ready external CLI only for `Complex` work, keeping
//! routine tasks on the native loop. Detection is injected as plain data so
//! resolution is unit-testable without binaries on `$PATH`.

use car_inference::adaptive_router::TaskComplexity;
use serde::{Deserialize, Serialize};

/// Which engine performs the coding work.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EngineChoice {
    /// Decide per task: complexity assessment + detected external CLIs.
    Auto,
    /// CAR's own inference + tool loop.
    Native,
    /// One session of a detected external agentic CLI (`claude-code`,
    /// `codex`, `gemini`).
    External(String),
    /// Foreman (#274): decompose the intent, farm subtasks to the external
    /// CLI in parallel worktrees, gate each patch + the integrated union,
    /// then land the verified union in the session worktree. Declines to
    /// single-session/native when the plan has no parallelism.
    Foreman(String),
}

impl EngineChoice {
    /// Parse the RPC/CLI form: `auto` | `native` | `external[:agent_id]` |
    /// `foreman[:agent_id]`.
    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}"),
        }
    }
}

/// What resolution needs to know about one detected external CLI.
#[derive(Debug, Clone)]
pub struct DetectedAgent {
    pub id: String,
    /// Health bucket is `Ready` (authenticated, status command succeeded).
    pub ready: bool,
}

/// Default delegation preference when several CLIs are ready.
pub const DEFAULT_PREFERENCE: [&str; 3] = ["claude-code", "codex", "gemini"];

/// A resolved engine (never `Auto`) plus the reason, for the event stream.
#[derive(Debug, Clone, PartialEq)]
pub struct ResolvedEngine {
    pub engine: EngineChoice,
    pub reason: String,
}

/// Snapshot the currently-detected external CLIs (with health) into the
/// plain shape [`resolve_engine`] consumes.
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())
        // A ready agent outside the preference list still beats nothing.
        .or_else(|| detected.iter().find(|d| d.ready).map(|d| d.id.clone()))
}

/// Resolve the requested engine against the detected CLIs and the intent's
/// assessed complexity. Errors only when an *explicit* external request can't
/// be satisfied — `Auto` always resolves (falling back to native).
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 => {
            // `assess` buckets almost any coding intent as `Code` (repair
            // markers like "fix"/"refactor" dominate), so `Code` alone can't
            // mean "delegate". Frontier-worthy = genuinely Complex, or a Code
            // task whose intent is long/multi-step enough that a frontier CLI
            // is likely to outperform the native loop. The explicit engine
            // override is the real control; this is a default, not a promise.
            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) {
                    // Foreman-first: its planner self-selects — a plan with
                    // no parallelism declines to single-session, so trivial
                    // tasks never pay the farm-out cost.
                    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()
    }

    // A prompt TaskComplexity::assess reliably buckets as Complex: long,
    // multi-step, architecture-flavored.
    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());
    }
}