collet 0.1.1

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
//! Collaboration mode configuration.
//!
//! Defines a tiered collaboration model:
//!   None < Fork < Hive < Flock
//!
//! - **None**  : single agent, sequential execution
//! - **Fork**  : coordinator splits work, agents run in parallel, coordinator merges results
//! - **Hive**  : agents communicate peer-to-peer for consensus; coordinator only monitors
//! - **Flock** : real-time inter-agent messaging (à la AutoGen / Claude Code Agent Flocks)

use serde::{Deserialize, Serialize};

// ---------------------------------------------------------------------------
// CollaborationMode enum
// ---------------------------------------------------------------------------

/// Tiered collaboration mode.  Higher tiers include lower-tier capabilities.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CollaborationMode {
    /// Single-agent sequential execution (default).
    #[default]
    None,
    /// Coordinator splits task → agents execute in parallel → coordinator merges.
    Fork,
    /// Agents communicate peer-to-peer for consensus; coordinator only monitors.
    Hive,
    /// Real-time inter-agent messaging (future / experimental).
    Flock,
}

impl CollaborationMode {
    /// Numeric tier: None=0, Fork=1, Hive=2, Flock=3.
    /// Higher tiers include all lower-tier capabilities.
    pub fn tier(&self) -> u8 {
        match self {
            Self::None => 0,
            Self::Fork => 1,
            Self::Hive => 2,
            Self::Flock => 3,
        }
    }

    /// Returns true when the mode requires a coordinator to split/merge work.
    /// (tier >= Fork)
    pub fn is_parallel(&self) -> bool {
        self.tier() >= 1
    }

    /// Returns true when consensus/voting/plan-review capabilities are enabled.
    /// (tier >= Hive)
    pub fn has_consensus(&self) -> bool {
        self.tier() >= 2
    }

    /// Returns true when real-time blackboard claims and announcements are enabled.
    /// (tier >= Flock)
    pub fn has_realtime(&self) -> bool {
        self.tier() >= 3
    }

    /// Human-readable label.
    pub fn label(&self) -> &'static str {
        match self {
            Self::None => "none",
            Self::Fork => "fork",
            Self::Hive => "hive",
            Self::Flock => "flock",
        }
    }
}

impl std::fmt::Display for CollaborationMode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.label())
    }
}

// ---------------------------------------------------------------------------
// Supporting enums (shared by Hive and Flock modes)
// ---------------------------------------------------------------------------

/// Coordination strategy for hive/flock modes.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SwarmStrategy {
    /// Coordinator analyzes task, splits into subtasks, agents execute in parallel.
    #[default]
    #[serde(alias = "autosplit")]
    AutoSplit,
    /// User defines roles explicitly via config.
    RoleBased,
    /// Architect proposes plan, reviewers review, then workers execute.
    PlanReviewExecute,
}

/// Conflict resolution strategy when multiple agents modify the same file.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConflictResolution {
    /// Last write wins (simple, fast).
    LastWriterWins,
    /// Coordinator agent resolves conflicts using LLM judgment.
    #[default]
    CoordinatorResolves,
    /// User is prompted to resolve conflicts manually.
    UserResolves,
}

// ---------------------------------------------------------------------------
// CollaborationConfig (runtime — fully resolved)
// ---------------------------------------------------------------------------

/// Runtime collaboration configuration.
#[derive(Debug, Clone)]
pub struct CollaborationConfig {
    /// Active collaboration mode.
    pub mode: CollaborationMode,
    /// Maximum number of concurrent agents (default: 3).
    pub max_agents: usize,
    /// Model to use for worker agents (falls back to `config.model`).
    pub worker_model: Option<String>,
    /// Model to use for the coordinator agent (falls back to `config.model`).
    pub coordinator_model: Option<String>,
    /// Use git worktree isolation for each parallel agent (default: false).
    /// File-level claiming prevents concurrent writes without worktree overhead.
    pub worktree: bool,
    /// Automatically suggest upgrading the mode when a complex task is detected (default: true).
    pub auto_suggest: bool,
    /// Require consensus among agents before convergence (hive/flock only; default: true).
    pub require_consensus: bool,
    /// Conflict resolution strategy (default: coordinator_resolves).
    pub conflict_resolution: ConflictResolution,
    /// Coordination strategy (default: auto_split).
    pub strategy: SwarmStrategy,
}

impl Default for CollaborationConfig {
    fn default() -> Self {
        Self {
            mode: CollaborationMode::None,
            max_agents: 3,
            worker_model: None,
            coordinator_model: None,
            worktree: false,
            auto_suggest: true,
            require_consensus: true,
            conflict_resolution: ConflictResolution::default(),
            strategy: SwarmStrategy::default(),
        }
    }
}

impl CollaborationConfig {
    /// Build a client for the coordinator, applying `coordinator_model`.
    ///
    /// Accepts both `"model"` and `"provider/model"` formats.
    /// When a provider prefix is present the client's base_url and api_key
    /// are switched to that provider; otherwise only the model name changes.
    pub fn coordinator_client(
        &self,
        base: &crate::api::provider::OpenAiCompatibleProvider,
    ) -> crate::api::provider::OpenAiCompatibleProvider {
        Self::apply_model_spec(base, self.coordinator_model.as_deref())
    }

    /// Build a client for worker agents, applying `worker_model`.
    ///
    /// Accepts both `"model"` and `"provider/model"` formats.
    pub fn worker_client(
        &self,
        base: &crate::api::provider::OpenAiCompatibleProvider,
    ) -> crate::api::provider::OpenAiCompatibleProvider {
        Self::apply_model_spec(base, self.worker_model.as_deref())
    }

    /// Parse `"[provider/]model"` and return a client with the correct
    /// base_url, api_key, and model applied.
    fn apply_model_spec(
        base: &crate::api::provider::OpenAiCompatibleProvider,
        spec: Option<&str>,
    ) -> crate::api::provider::OpenAiCompatibleProvider {
        let Some(spec) = spec else {
            return base.clone();
        };
        let mut client = base.clone();
        if let Some(slash) = spec.find('/') {
            let provider_name = &spec[..slash];
            let model = &spec[slash + 1..];
            if let Some((entry, api_key)) = crate::config::resolve_provider(provider_name) {
                if !entry.base_url.is_empty() {
                    let profile = crate::api::model_profile::profile_for(model);
                    client.switch_provider(
                        entry.base_url,
                        api_key,
                        model.to_string(),
                        profile.max_output_tokens,
                    );
                } else {
                    client.model = model.to_string();
                }
            } else {
                client.model = model.to_string();
            }
        } else {
            client.model = spec.to_string();
        }
        client
    }

    /// Resolve from TOML section + environment variables.
    /// Build a `CollaborationConfig` from the TOML section.
    ///
    /// `provider` is the active provider entry — used to resolve `worker_model` and
    /// `coordinator_model` from the provider's ordered `models` list when they are
    /// not explicitly set in config. Pass `None` to fall back to the main model.
    pub fn from_section(
        section: &CollaborationSection,
        provider: Option<&crate::config::types::ProviderEntry>,
    ) -> Self {
        let mode = std::env::var("COLLET_COLLAB_MODE")
            .ok()
            .and_then(|v| match v.to_lowercase().as_str() {
                "fork" => Some(CollaborationMode::Fork),
                "hive" => Some(CollaborationMode::Hive),
                "flock" => Some(CollaborationMode::Flock),
                "none" | "" => Some(CollaborationMode::None),
                _ => None,
            })
            .or_else(|| section.mode.clone())
            .unwrap_or_default();

        let max_agents = std::env::var("COLLET_COLLAB_MAX_AGENTS")
            .ok()
            .and_then(|v| v.parse().ok())
            .or(section.max_agents)
            .unwrap_or(3);

        let worktree = std::env::var("COLLET_COLLAB_WORKTREE")
            .ok()
            .and_then(|v| match v.as_str() {
                "1" | "true" => Some(true),
                "0" | "false" => Some(false),
                _ => None,
            })
            .or(section.worktree)
            .unwrap_or(false);

        let worker_model = section.worker_model.clone().or_else(|| {
            provider.and_then(|p| {
                p.model_for_role(crate::config::types::ModelRole::Worker)
                    .map(str::to_owned)
            })
        });
        let coordinator_model = section.coordinator_model.clone().or_else(|| {
            provider.and_then(|p| {
                p.model_for_role(crate::config::types::ModelRole::Coordinator)
                    .map(str::to_owned)
            })
        });

        Self {
            mode,
            max_agents,
            worker_model,
            coordinator_model,
            worktree,
            auto_suggest: section.auto_suggest.unwrap_or(true),
            require_consensus: section.require_consensus.unwrap_or(true),
            conflict_resolution: section.conflict_resolution.clone().unwrap_or_default(),
            strategy: section.strategy.clone().unwrap_or_default(),
        }
    }
}

// ---------------------------------------------------------------------------
// CollaborationSection (TOML deserialization schema)
// ---------------------------------------------------------------------------

/// TOML `[collaboration]` section.
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct CollaborationSection {
    /// Collaboration mode: "none" | "fork" | "hive" | "flock"
    pub mode: Option<CollaborationMode>,
    pub max_agents: Option<usize>,
    pub worker_model: Option<String>,
    pub coordinator_model: Option<String>,
    pub worktree: Option<bool>,
    pub auto_suggest: Option<bool>,
    pub require_consensus: Option<bool>,
    pub conflict_resolution: Option<ConflictResolution>,
    pub strategy: Option<SwarmStrategy>,
}

// ---------------------------------------------------------------------------
// Backward-compat type aliases (deprecated — will be removed in future)
// ---------------------------------------------------------------------------

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

    #[test]
    fn test_default_collab_config() {
        let cfg = CollaborationConfig::default();
        assert_eq!(cfg.mode, CollaborationMode::None);
        assert_eq!(cfg.max_agents, 3);
        assert!(!cfg.worktree);
        assert!(cfg.auto_suggest);
        assert!(cfg.require_consensus);
        assert_eq!(
            cfg.conflict_resolution,
            ConflictResolution::CoordinatorResolves
        );
        assert_eq!(cfg.strategy, SwarmStrategy::AutoSplit);
    }

    #[test]
    fn test_collab_from_section_defaults() {
        let section = CollaborationSection::default();
        let cfg = CollaborationConfig::from_section(&section, None);
        assert_eq!(cfg.mode, CollaborationMode::None);
        assert_eq!(cfg.max_agents, 3);
    }

    #[test]
    fn test_collab_from_section_fork() {
        let section = CollaborationSection {
            mode: Some(CollaborationMode::Fork),
            max_agents: Some(5),
            worker_model: Some("glm-4.7-flash".to_string()),
            worktree: Some(false),
            ..Default::default()
        };
        let cfg = CollaborationConfig::from_section(&section, None);
        assert_eq!(cfg.mode, CollaborationMode::Fork);
        assert_eq!(cfg.max_agents, 5);
        assert_eq!(cfg.worker_model.as_deref(), Some("glm-4.7-flash"));
        assert!(!cfg.worktree);
    }

    #[test]
    fn test_collab_from_section_hive() {
        let section = CollaborationSection {
            mode: Some(CollaborationMode::Hive),
            max_agents: Some(4),
            strategy: Some(SwarmStrategy::PlanReviewExecute),
            coordinator_model: Some("glm-5".to_string()),
            require_consensus: Some(false),
            conflict_resolution: Some(ConflictResolution::UserResolves),
            ..Default::default()
        };
        let cfg = CollaborationConfig::from_section(&section, None);
        assert_eq!(cfg.mode, CollaborationMode::Hive);
        assert_eq!(cfg.max_agents, 4);
        assert_eq!(cfg.strategy, SwarmStrategy::PlanReviewExecute);
        assert_eq!(cfg.coordinator_model.as_deref(), Some("glm-5"));
        assert!(!cfg.require_consensus);
        assert_eq!(cfg.conflict_resolution, ConflictResolution::UserResolves);
    }

    #[test]
    fn test_collab_section_serde() {
        let toml_str = r#"
mode = "hive"
max_agents = 4
strategy = "plan_review_execute"
coordinator_model = "glm-5"
conflict_resolution = "last_writer_wins"
"#;
        let section: CollaborationSection = toml::from_str(toml_str).unwrap();
        assert_eq!(section.mode, Some(CollaborationMode::Hive));
        assert_eq!(section.max_agents, Some(4));
        assert_eq!(section.strategy, Some(SwarmStrategy::PlanReviewExecute));
        assert_eq!(
            section.conflict_resolution,
            Some(ConflictResolution::LastWriterWins)
        );
    }

    #[test]
    fn test_mode_is_parallel() {
        assert!(!CollaborationMode::None.is_parallel());
        assert!(CollaborationMode::Fork.is_parallel());
        assert!(CollaborationMode::Hive.is_parallel());
        assert!(CollaborationMode::Flock.is_parallel());
    }
}