mars-agents 0.4.8-rc.4

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

use crate::harness::registry::HarnessId;

use super::Settings;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RoutingConfigDiagnostic {
    pub message: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedRoutingSettings {
    pub harness_order: Option<ParsedHarnessOrder>,
    pub default_harness: Option<ParsedHarnessValue>,
    pub linked_harnesses: BTreeSet<HarnessId>,
    pub diagnostics: Vec<RoutingConfigDiagnostic>,
}

impl ResolvedRoutingSettings {
    pub fn harness_order_names(&self) -> Option<Vec<String>> {
        self.harness_order.as_ref().map(|order| {
            order
                .candidates
                .iter()
                .map(|c| c.harness.to_string())
                .collect()
        })
    }

    pub fn default_harness_name(&self) -> Option<String> {
        self.default_harness
            .as_ref()
            .map(|value| value.harness.to_string())
    }

    pub fn linked_harness_names(&self) -> Vec<String> {
        self.linked_harnesses
            .iter()
            .map(|harness| harness.to_string())
            .collect()
    }

    pub fn diagnostic_messages(&self) -> Vec<String> {
        self.diagnostics
            .iter()
            .map(|diagnostic| diagnostic.message.clone())
            .collect()
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedHarnessValue {
    pub harness: HarnessId,
    pub original: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedHarnessOrder {
    pub candidates: Vec<OrderedHarnessCandidate>,
    pub failure: Option<HarnessOrderFailure>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HarnessOrderFailure {
    Empty,
    AllInvalid,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OrderedHarnessCandidate {
    pub harness: HarnessId,
    pub original: String,
    pub position: usize,
}

pub fn resolve(settings: &Settings) -> ResolvedRoutingSettings {
    let mut diagnostics = Vec::new();

    let harness_order = settings.harness_order.as_ref().map(|order| {
        if order.is_empty() {
            diagnostics.push(RoutingConfigDiagnostic {
                message:
                    "settings.harness_order is empty; falling through to provider candidate order"
                        .to_string(),
            });
            return ParsedHarnessOrder {
                candidates: Vec::new(),
                failure: Some(HarnessOrderFailure::Empty),
            };
        }

        let mut candidates = Vec::new();
        for (position, candidate) in order.iter().enumerate() {
            let Some(harness) = crate::harness::registry::parse(candidate) else {
                diagnostics.push(RoutingConfigDiagnostic {
                    message: format!(
                        "settings.harness_order contains unrecognized harness `{candidate}`; skipping (valid: {})",
                        crate::harness::registry::names().join(", ")
                    ),
                });
                continue;
            };
            candidates.push(OrderedHarnessCandidate {
                harness,
                original: candidate.clone(),
                position,
            });
        }

        let failure = if candidates.is_empty() {
            diagnostics.push(RoutingConfigDiagnostic {
                message:
                    "settings.harness_order has no valid candidates; falling through to provider candidate order"
                        .to_string(),
            });
            Some(HarnessOrderFailure::AllInvalid)
        } else {
            None
        };

        ParsedHarnessOrder {
            candidates,
            failure,
        }
    });

    let default_harness = settings.default_harness.as_ref().and_then(|value| {
        match crate::harness::registry::parse(value) {
            Some(harness) => Some(ParsedHarnessValue {
                harness,
                original: value.clone(),
            }),
            None => {
                diagnostics.push(RoutingConfigDiagnostic {
                    message: format!(
                        "settings.default_harness `{value}` is invalid; expected one of: {}",
                        crate::harness::registry::names().join(", ")
                    ),
                });
                None
            }
        }
    });

    let linked_harnesses = settings.effective_links().linked_harnesses_set();

    ResolvedRoutingSettings {
        harness_order,
        default_harness,
        linked_harnesses,
        diagnostics,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn settings_with_links(targets: Option<Vec<&str>>) -> Settings {
        Settings {
            managed_root: None,
            targets: targets.map(|values| values.into_iter().map(str::to_string).collect()),
            ..Settings::default()
        }
    }

    #[test]
    fn parse_invalid_default_harness_to_diagnostic() {
        let mut settings = settings_with_links(None);
        settings.default_harness = Some("gemini".to_string());

        let resolved = resolve(&settings);
        assert!(resolved.default_harness.is_none());
        assert!(
            resolved
                .diagnostics
                .iter()
                .any(|diagnostic| diagnostic.message.contains("settings.default_harness"))
        );
    }

    #[test]
    fn parse_harness_order_preserves_original_positions() {
        let mut settings = settings_with_links(None);
        settings.harness_order = Some(vec!["pi".to_string(), "codex".to_string()]);

        let resolved = resolve(&settings);
        let order = resolved.harness_order.expect("harness order should be set");
        assert_eq!(order.candidates[0].harness, HarnessId::Pi);
        assert_eq!(order.candidates[0].position, 0);
        assert_eq!(order.candidates[1].harness, HarnessId::Codex);
        assert_eq!(order.candidates[1].position, 1);
    }

    #[test]
    fn linked_harnesses_come_from_known_links_only() {
        let settings = settings_with_links(Some(vec![".opencode", ".agents", "foo/bar"]));
        let resolved = resolve(&settings);

        assert!(resolved.linked_harnesses.contains(&HarnessId::OpenCode));
        assert!(!resolved.linked_harnesses.contains(&HarnessId::Codex));
    }
}