mars-agents 0.4.8-rc.1

Agent package manager for .agents/ directories
Documentation
use std::collections::HashSet;

use crate::build::policy::PolicyInput;
use crate::compiler::agents::HarnessKind;
use crate::error::{ConfigError, MarsError};
use crate::models;
use crate::models::ModelAlias;
use crate::models::harness::HarnessOrderFailure;

pub(super) struct HarnessResolution {
    pub(super) harness: String,
    pub(super) source: &'static str,
    pub(super) harness_order_position: Option<usize>,
    pub(super) is_experimental: bool,
    pub(super) resolved_harness: HarnessKind,
    pub(super) warnings: Vec<String>,
}

struct CandidateHarnessResolution {
    harness: String,
    source: &'static str,
    harness_order_position: Option<usize>,
    warnings: Vec<String>,
}

pub(super) fn resolve_harness(
    input: &PolicyInput<'_>,
    alias: Option<&ModelAlias>,
    provider: Option<&str>,
    config_default_harness: Option<&str>,
    harness_order: Option<&[String]>,
) -> Result<HarnessResolution, MarsError> {
    let mut warnings = Vec::new();

    let profile_harness = input.profile.harness.as_ref().map(harness_kind_to_str);
    let alias_harness = alias.and_then(|entry| entry.harness.as_deref());
    let installed_harnesses = models::harness::detect_installed_harnesses();
    let normalized_config_default_harness =
        normalize_config_default_harness(config_default_harness, &mut warnings);

    let model_from_cli = input.model_override.is_some();
    let mut selected_harness_order_position = None;
    let (harness, harness_source) = if let Some(harness) = input.harness_override {
        (harness.to_string(), "cli")
    } else if model_from_cli {
        if let Some(harness) = alias_harness {
            (harness.to_string(), "alias")
        } else {
            let resolved = resolve_harness_candidate_or_fallback(
                provider,
                harness_order,
                &installed_harnesses,
                normalized_config_default_harness.clone(),
            );
            selected_harness_order_position = resolved.harness_order_position;
            warnings.extend(resolved.warnings);
            (resolved.harness, resolved.source)
        }
    } else if let Some(harness) = profile_harness {
        (harness.to_string(), "profile")
    } else if let Some(harness) = alias_harness {
        (harness.to_string(), "alias")
    } else {
        let resolved = resolve_harness_candidate_or_fallback(
            provider,
            harness_order,
            &installed_harnesses,
            normalized_config_default_harness,
        );
        selected_harness_order_position = resolved.harness_order_position;
        warnings.extend(resolved.warnings);
        (resolved.harness, resolved.source)
    };

    let resolved_harness = HarnessKind::from_str(&harness).ok_or_else(|| {
        MarsError::Config(ConfigError::Invalid {
            message: format!(
                "resolved harness `{harness}` is invalid; expected one of: claude, codex, opencode, cursor, pi"
            ),
        })
    })?;

    Ok(HarnessResolution {
        is_experimental: harness == "cursor",
        resolved_harness,
        harness,
        source: harness_source,
        harness_order_position: selected_harness_order_position,
        warnings,
    })
}

fn normalize_config_default_harness(
    config_default_harness: Option<&str>,
    warnings: &mut Vec<String>,
) -> Option<String> {
    match config_default_harness {
        Some(value) => match normalize_harness_name(value) {
            Some(valid) => Some(valid.to_string()),
            None => {
                warnings.push(format!(
                    "settings.default_harness `{value}` is invalid; expected one of: claude, codex, opencode, cursor, pi"
                ));
                None
            }
        },
        None => None,
    }
}

fn resolve_harness_candidate_or_fallback(
    provider: Option<&str>,
    settings_harness_order: Option<&[String]>,
    installed_harnesses: &HashSet<String>,
    config_default_harness: Option<String>,
) -> CandidateHarnessResolution {
    let mut candidate = models::harness::resolve_harness_from_candidates(
        provider,
        settings_harness_order,
        installed_harnesses,
    );
    if let Some(harness) = candidate.harness {
        return CandidateHarnessResolution {
            harness,
            source: candidate.source.unwrap_or("provider"),
            harness_order_position: candidate.harness_order_position,
            warnings: candidate.warnings,
        };
    }

    if settings_harness_order.is_some()
        && let Some(warning) = format_harness_order_fallback_warning(
            candidate.harness_order_failure.as_ref(),
            config_default_harness.is_some(),
        )
    {
        candidate.warnings.push(warning);
    }

    if let Some(harness) = config_default_harness {
        return CandidateHarnessResolution {
            harness,
            source: "config",
            harness_order_position: None,
            warnings: candidate.warnings,
        };
    }

    let mut warnings = candidate.warnings;
    warnings.push(
        "harness not set by CLI/profile/alias/provider/config; defaulting to `claude`".to_string(),
    );
    CandidateHarnessResolution {
        harness: "claude".to_string(),
        source: "default",
        harness_order_position: None,
        warnings,
    }
}

fn format_harness_order_fallback_warning(
    harness_order_failure: Option<&HarnessOrderFailure>,
    has_config_default_harness: bool,
) -> Option<String> {
    let mut warning = match harness_order_failure {
        Some(HarnessOrderFailure::Empty) => "settings.harness_order is empty".to_string(),
        Some(HarnessOrderFailure::NoneInstalled { valid_candidates }) => format!(
            "settings.harness_order is set but none of [{}] are installed",
            valid_candidates.join(", ")
        ),
        None => return None,
    };

    if has_config_default_harness {
        warning.push_str("; falling through to settings.default_harness");
    } else {
        warning
            .push_str("; settings.default_harness is unset, falling through to hardcoded `claude`");
    }

    Some(warning)
}

fn normalize_harness_name(value: &str) -> Option<&'static str> {
    match value.trim() {
        "claude" => Some("claude"),
        "codex" => Some("codex"),
        "opencode" => Some("opencode"),
        "cursor" => Some("cursor"),
        "pi" => Some("pi"),
        _ => None,
    }
}

pub(super) fn harness_kind_to_str(harness: &HarnessKind) -> &'static str {
    match harness {
        HarnessKind::Claude => "claude",
        HarnessKind::Codex => "codex",
        HarnessKind::OpenCode => "opencode",
        HarnessKind::Cursor => "cursor",
        HarnessKind::Pi => "pi",
    }
}