use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tracing::Instrument;
use crate::agent::Agent;
use crate::error::KovaError;
pub enum OrchestratorPattern {
Sequential(Vec<String>),
Parallel(Vec<String>),
Router {
router_agent: String,
downstream: Vec<String>,
},
}
pub struct ParallelResult {
pub successes: Vec<(String, String)>,
pub failures: Vec<(String, KovaError)>,
}
pub struct Orchestrator {
agents: HashMap<String, Arc<Agent>>,
timeout: Duration,
}
impl Orchestrator {
pub fn new(agents: HashMap<String, Arc<Agent>>, timeout: Duration) -> Self {
Self { agents, timeout }
}
pub async fn execute(
&self,
pattern: OrchestratorPattern,
input: &str,
) -> Result<OrchestratorOutput, KovaError> {
let pattern_type = match &pattern {
OrchestratorPattern::Sequential(_) => "sequential",
OrchestratorPattern::Parallel(_) => "parallel",
OrchestratorPattern::Router { .. } => "router",
};
let span = tracing::info_span!(
"orchestrator.execute",
pattern = pattern_type,
otel.status_code = tracing::field::Empty,
);
async {
let result = tokio::time::timeout(self.timeout, self.execute_inner(pattern, input))
.await
.map_err(|_| {
tracing::Span::current().record("otel.status_code", "ERROR");
tracing::warn!("Orchestration timed out");
KovaError::Orchestration(format!(
"Orchestration timed out after {:?}",
self.timeout
))
})?;
if result.is_err() {
tracing::Span::current().record("otel.status_code", "ERROR");
}
result
}
.instrument(span)
.await
}
async fn execute_inner(
&self,
pattern: OrchestratorPattern,
input: &str,
) -> Result<OrchestratorOutput, KovaError> {
match pattern {
OrchestratorPattern::Sequential(agent_names) => {
self.execute_sequential(&agent_names, input).await
}
OrchestratorPattern::Parallel(agent_names) => {
self.execute_parallel(&agent_names, input).await
}
OrchestratorPattern::Router {
router_agent,
downstream,
} => self.execute_router(&router_agent, &downstream, input).await,
}
}
fn get_agent(&self, name: &str) -> Result<&Arc<Agent>, KovaError> {
self.agents.get(name).ok_or_else(|| {
KovaError::Orchestration(format!("Agent '{}' not found in orchestrator", name))
})
}
async fn execute_sequential(
&self,
agent_names: &[String],
input: &str,
) -> Result<OrchestratorOutput, KovaError> {
if agent_names.is_empty() {
return Err(KovaError::Orchestration(
"Sequential pattern requires at least one agent".to_string(),
));
}
let mut current_input = input.to_string();
for (i, name) in agent_names.iter().enumerate() {
let agent = self.get_agent(name)?;
let conversation_id = format!("orch-seq-{}-{}", name, i);
current_input = agent
.chat(&conversation_id, ¤t_input)
.await
.map_err(|e| {
KovaError::Orchestration(format!(
"Agent '{}' failed in sequential pipeline: {}",
name, e
))
})?;
}
Ok(OrchestratorOutput::Single(current_input))
}
async fn execute_parallel(
&self,
agent_names: &[String],
input: &str,
) -> Result<OrchestratorOutput, KovaError> {
if agent_names.is_empty() {
return Err(KovaError::Orchestration(
"Parallel pattern requires at least one agent".to_string(),
));
}
let agents: Vec<(String, Arc<Agent>)> = agent_names
.iter()
.map(|name| self.get_agent(name).map(|a| (name.clone(), Arc::clone(a))))
.collect::<Result<Vec<_>, _>>()?;
let mut handles = Vec::with_capacity(agents.len());
for (name, agent) in agents {
let input_owned = input.to_string();
let conv_id = format!("orch-par-{}", name);
let agent_span = tracing::info_span!(
"orchestrator.agent",
agent.name = %name,
);
handles.push(tokio::spawn(
async move {
let result = agent.chat(&conv_id, &input_owned).await;
(name, result)
}
.instrument(agent_span),
));
}
let mut successes = Vec::new();
let mut failures = Vec::new();
for handle in handles {
match handle.await {
Ok((name, Ok(output))) => successes.push((name, output)),
Ok((name, Err(e))) => failures.push((name, e)),
Err(join_err) => {
failures.push((
"unknown".to_string(),
KovaError::Orchestration(format!("Task join error: {}", join_err)),
));
}
}
}
Ok(OrchestratorOutput::Parallel(ParallelResult {
successes,
failures,
}))
}
async fn execute_router(
&self,
router_name: &str,
downstream: &[String],
input: &str,
) -> Result<OrchestratorOutput, KovaError> {
let router = self.get_agent(router_name)?;
let conv_id = format!("orch-router-{}", router_name);
let selected_name = router.chat(&conv_id, input).await.map_err(|e| {
KovaError::Orchestration(format!("Router agent '{}' failed: {}", router_name, e))
})?;
let selected_name = selected_name.trim().to_string();
if !downstream.contains(&selected_name) {
return Err(KovaError::Orchestration(format!(
"Router selected '{}' which is not in downstream agents: {:?}",
selected_name, downstream
)));
}
let agent = self.get_agent(&selected_name)?;
let downstream_conv_id = format!("orch-routed-{}", selected_name);
let output = agent.chat(&downstream_conv_id, input).await.map_err(|e| {
KovaError::Orchestration(format!(
"Downstream agent '{}' failed: {}",
selected_name, e
))
})?;
Ok(OrchestratorOutput::Single(output))
}
}
pub enum OrchestratorOutput {
Single(String),
Parallel(ParallelResult),
}