use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CollaborationMode {
#[default]
None,
Fork,
Hive,
Flock,
}
impl CollaborationMode {
pub fn tier(&self) -> u8 {
match self {
Self::None => 0,
Self::Fork => 1,
Self::Hive => 2,
Self::Flock => 3,
}
}
pub fn is_parallel(&self) -> bool {
self.tier() >= 1
}
pub fn has_consensus(&self) -> bool {
self.tier() >= 2
}
pub fn has_realtime(&self) -> bool {
self.tier() >= 3
}
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())
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SwarmStrategy {
#[default]
#[serde(alias = "autosplit")]
AutoSplit,
RoleBased,
PlanReviewExecute,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConflictResolution {
LastWriterWins,
#[default]
CoordinatorResolves,
UserResolves,
}
#[derive(Debug, Clone)]
pub struct CollaborationConfig {
pub mode: CollaborationMode,
pub max_agents: usize,
pub worker_model: Option<String>,
pub coordinator_model: Option<String>,
pub worktree: bool,
pub auto_suggest: bool,
pub require_consensus: bool,
pub conflict_resolution: ConflictResolution,
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 {
pub fn coordinator_client(
&self,
base: &crate::api::provider::OpenAiCompatibleProvider,
) -> crate::api::provider::OpenAiCompatibleProvider {
Self::apply_model_spec(base, self.coordinator_model.as_deref())
}
pub fn worker_client(
&self,
base: &crate::api::provider::OpenAiCompatibleProvider,
) -> crate::api::provider::OpenAiCompatibleProvider {
Self::apply_model_spec(base, self.worker_model.as_deref())
}
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
}
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(),
}
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct CollaborationSection {
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>,
}
#[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(§ion, 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(§ion, 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(§ion, 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());
}
}