use warp_multi_agent_api as api; use super::action::RunAgentsRequest; /// Client-side representation of the orchestration config attached to a /// conversation via `OrchestrationConfigSnapshot`. /// /// Mirrors the proto `OrchestrationConfig` but uses Rust-native types /// to keep view / model code free of proto imports. #[derive(Debug, Clone, Eq, PartialEq)] pub struct OrchestrationConfig { pub model_id: String, pub harness_type: String, pub execution_mode: OrchestrationExecutionMode, } #[derive(Debug, Clone, Eq, PartialEq)] pub enum OrchestrationExecutionMode { Local, Remote { environment_id: String, worker_host: String, }, } impl OrchestrationExecutionMode { pub fn is_remote(&self) -> bool { matches!(self, Self::Remote { .. }) } } /// User's approval state for orchestration on the active config. #[derive(Debug, Clone, Copy, Eq, PartialEq, Default)] pub enum OrchestrationConfigStatus { /// No `OrchestrationConfigSnapshot` has been seen yet. #[default] None, Approved, Disapproved, } impl OrchestrationConfigStatus { pub fn is_approved(&self) -> bool { matches!(self, Self::Approved) } pub fn is_disapproved(&self) -> bool { matches!(self, Self::Disapproved) } } // --------------------------------------------------------------------------- // Match check — determines whether a `run_agents` call auto-launches. // --------------------------------------------------------------------------- /// Returns `true` when the `run_agents` call's run-wide fields match /// the active approved `OrchestrationConfig`, meaning the confirmation /// card can be skipped (auto-launch). /// /// Empty/unset fields on the call are treated as inheriting from the /// config (and therefore matching). Fields not in the config /// (`computer_use_enabled`, `skills`, `base_prompt`, `agent_run_configs`, /// per-agent `title`) are excluded from the check. pub fn matches_active_config(request: &RunAgentsRequest, config: &OrchestrationConfig) -> bool { // model_id — empty on the call means "inherit from config" → matches. if !request.model_id.is_empty() && request.model_id != config.model_id { return false; } // harness_type if !request.harness_type.is_empty() && request.harness_type != config.harness_type { return false; } // execution_mode variant must agree. match (&request.execution_mode, &config.execution_mode) { (super::action::RunAgentsExecutionMode::Local, OrchestrationExecutionMode::Local) => true, ( super::action::RunAgentsExecutionMode::Remote { environment_id, worker_host, .. }, OrchestrationExecutionMode::Remote { environment_id: cfg_env, worker_host: cfg_host, }, ) => { let env_matches = environment_id.is_empty() || environment_id == cfg_env; let host_matches = worker_host.is_empty() || worker_host == cfg_host; env_matches && host_matches } // Variant mismatch (Local vs Remote). _ => false, } } // --------------------------------------------------------------------------- // Proto ↔ native conversions // --------------------------------------------------------------------------- impl OrchestrationConfig { /// Converts from the proto `OrchestrationConfig` message. pub fn from_proto(proto: &api::OrchestrationConfig) -> Self { let execution_mode = match &proto.execution_mode { Some(api::orchestration_config::ExecutionMode::Remote(remote)) => { OrchestrationExecutionMode::Remote { environment_id: remote.environment_id.clone(), worker_host: remote.worker_host.clone(), } } Some(api::orchestration_config::ExecutionMode::Local(_)) | None => { OrchestrationExecutionMode::Local } }; let harness_type = harness_proto_to_string(proto.harness.as_ref()).unwrap_or_default(); Self { model_id: proto.model_id.clone(), harness_type, execution_mode, } } /// Converts to the proto `OrchestrationConfig` message. pub fn to_proto(&self) -> api::OrchestrationConfig { let execution_mode = match &self.execution_mode { OrchestrationExecutionMode::Local => { Some(api::orchestration_config::ExecutionMode::Local( api::orchestration_config::Local {}, )) } OrchestrationExecutionMode::Remote { environment_id, worker_host, } => Some(api::orchestration_config::ExecutionMode::Remote( api::orchestration_config::Remote { environment_id: environment_id.clone(), worker_host: worker_host.clone(), }, )), }; api::OrchestrationConfig { model_id: self.model_id.clone(), harness: harness_type_to_proto(&self.harness_type), execution_mode, } } } impl OrchestrationConfigStatus { /// Converts from the proto `OrchestrationStatus` message. pub fn from_proto(proto: Option<&api::OrchestrationStatus>) -> Self { let Some(status) = proto else { return Self::None; }; match &status.status { Some(api::orchestration_status::Status::Approved(_)) => Self::Approved, Some(api::orchestration_status::Status::Disapproved(_)) => Self::Disapproved, None => Self::None, } } /// Converts to the proto `OrchestrationStatus` message. pub fn to_proto(&self) -> Option { match self { Self::None => None, Self::Approved => Some(api::OrchestrationStatus { status: Some(api::orchestration_status::Status::Approved( api::orchestration_status::Approved {}, )), }), Self::Disapproved => Some(api::OrchestrationStatus { status: Some(api::orchestration_status::Status::Disapproved( api::orchestration_status::Disapproved {}, )), }), } } } /// Maps the proto `Harness` oneof to a client-side string identifier. /// Returns `None` for an unset variant. fn harness_proto_to_string(harness: Option<&api::Harness>) -> Option { let variant = harness?.variant.as_ref()?; Some( match variant { api::harness::Variant::Oz(_) => "oz", api::harness::Variant::ClaudeCode(_) => "claude", api::harness::Variant::OpenCode(_) => "opencode", api::harness::Variant::Gemini(_) => "gemini", api::harness::Variant::Codex(_) => "codex", } .to_string(), ) } /// Converts a client-side harness string identifier to the proto `Harness` /// oneof variant. Returns `None` for empty or unknown strings. fn harness_type_to_proto(harness_type: &str) -> Option { let variant = match harness_type { "oz" => api::harness::Variant::Oz(api::harness::Oz {}), "claude" => api::harness::Variant::ClaudeCode(api::harness::ClaudeCode {}), "opencode" => api::harness::Variant::OpenCode(api::harness::OpenCode {}), "gemini" => api::harness::Variant::Gemini(api::harness::Gemini {}), "codex" => api::harness::Variant::Codex(api::harness::Codex {}), _ => return None, }; Some(api::Harness { variant: Some(variant), }) } #[cfg(test)] #[path = "orchestration_config_tests.rs"] mod tests;