use crate::application::errors::handoff_error::HandoffError;
use crate::core::platform::container::autonomous_config::HandoffConfig;
use crate::core::platform::container::handoff::{HandoffContext, HandoffRecord, HandoffStrategy};
use crate::core::platform::container::paladin::Paladin;
use log::{debug, info, warn};
use paladin_ports::output::paladin_executor_port::PaladinExecutorPort;
use std::sync::Arc;
use tokio::time::{Duration, sleep};
pub struct HandoffService {
config: Arc<HandoffConfig>,
}
impl HandoffService {
pub fn new(config: Arc<HandoffConfig>) -> Option<Self> {
if !config.enabled {
return None;
}
Some(Self { config })
}
pub fn strategy(&self) -> &HandoffStrategy {
&self.config.strategy
}
pub fn max_depth(&self) -> u32 {
self.config.max_depth
}
pub fn should_handoff(&self, task: &str, confidence: f32, context: &HandoffContext) -> bool {
if context.depth >= self.config.max_depth {
debug!(
"Max handoff depth reached (depth={}, max={}), executing locally",
context.depth, self.config.max_depth
);
return false;
}
match &self.config.strategy {
HandoffStrategy::Automatic => {
if confidence < 0.5 {
info!(
"Low confidence ({:.2}), considering handoff for task: {}",
confidence, task
);
true
} else {
let is_complex = self.is_complex_task(task);
if is_complex && confidence < 0.8 {
info!(
"Complex task with moderate confidence ({:.2}), considering handoff: {}",
confidence, task
);
true
} else {
false
}
}
}
HandoffStrategy::Explicit => {
debug!("Explicit strategy: no automatic handoffs");
false
}
HandoffStrategy::Threshold { .. } => {
if let Some(threshold) = self.config.strategy.get_threshold() {
let should_handoff = confidence < threshold;
if should_handoff {
info!(
"Confidence ({:.2}) below threshold ({:.2}), handing off: {}",
confidence, threshold, task
);
}
should_handoff
} else {
false
}
}
}
}
fn is_complex_task(&self, task: &str) -> bool {
let task_lower = task.to_lowercase();
let complexity_keywords = [
"implement",
"design",
"architecture",
"distributed",
"algorithm",
"optimize",
"refactor",
"migrate",
"integrate",
"system",
];
complexity_keywords
.iter()
.any(|keyword| task_lower.contains(keyword))
}
pub fn select_agent(
&self,
task: &str,
available_agents: &[(String, String)],
) -> Option<String> {
if available_agents.is_empty() {
debug!("No available agents for handoff");
return None;
}
let task_lower = task.to_lowercase();
let task_words: Vec<&str> = task_lower.split_whitespace().collect();
let mut scored_agents: Vec<(&String, &String, usize)> = available_agents
.iter()
.map(|(name, desc)| {
let desc_lower = desc.to_lowercase();
let score = self.calculate_relevance_score(&task_words, &desc_lower);
(name, desc, score)
})
.collect();
scored_agents.sort_by_key(|b| std::cmp::Reverse(b.2));
if let Some((name, desc, score)) = scored_agents.first().filter(|(_, _, s)| *s > 0) {
info!(
"Selected agent '{}' with score {} for task: {}",
name, score, task
);
debug!("Agent description: {}", desc);
return Some((*name).clone());
}
debug!("No suitable specialist found for task: {}", task);
None
}
fn calculate_relevance_score(&self, task_words: &[&str], agent_desc: &str) -> usize {
task_words
.iter()
.filter(|word| word.len() > 3) .filter(|word| agent_desc.contains(*word))
.map(|word| word.len()) .sum()
}
pub fn validate_handoff(
&self,
target_agent: &str,
context: &HandoffContext,
) -> Result<(), HandoffError> {
if context.depth >= self.config.max_depth {
return Err(HandoffError::MaxDepthExceeded {
current: context.depth,
max: self.config.max_depth,
});
}
if context.chain.iter().any(|agent| agent == target_agent) {
return Err(HandoffError::CircularHandoff {
agent_name: target_agent.to_string(),
chain: context.chain.join(" -> "),
});
}
Ok(())
}
pub fn can_handoff_to(&self, target_agent: &str, context: &HandoffContext) -> bool {
if context.depth >= self.config.max_depth {
return false;
}
!context.chain.iter().any(|agent| agent == target_agent)
}
pub fn transfer_context(
&self,
new_task: &str,
current_context: &HandoffContext,
target_agent: &str,
) -> HandoffContext {
let mut new_chain = current_context.chain.clone();
new_chain.push(target_agent.to_string());
HandoffContext {
task: new_task.to_string(),
chain: new_chain,
history: current_context.history.clone(),
metadata: current_context.metadata.clone(),
depth: current_context.depth + 1,
}
}
fn is_transient_error(error: &HandoffError) -> bool {
match error {
HandoffError::Timeout(_) => true,
HandoffError::ExecutionFailed { reason, .. } => {
let reason_lower = reason.to_lowercase();
reason_lower.contains("network")
|| reason_lower.contains("temporary")
|| reason_lower.contains("unavailable")
|| reason_lower.contains("timeout")
}
_ => false,
}
}
pub async fn execute_handoff(
&self,
specialist_name: &str,
task: &str,
context: &HandoffContext,
specialist: &Paladin,
executor: &dyn PaladinExecutorPort,
) -> Result<(String, HandoffRecord), HandoffError> {
info!(
"Executing handoff: from_chain={:?}, to={}, task_len={}, depth={}",
context.chain,
specialist_name,
task.len(),
context.depth
);
self.validate_handoff(specialist_name, context)?;
let new_context = self.transfer_context(task, context, specialist_name);
debug!(
"Context transferred: new_depth={}, chain={:?}",
new_context.depth, new_context.chain
);
let from_agent = context
.chain
.last()
.cloned()
.unwrap_or_else(|| "unknown".to_string());
let mut record = HandoffRecord::new(
from_agent.clone(),
specialist_name.to_string(),
task.to_string(),
new_context.depth,
);
let max_retries = self.config.retry.max_retries;
let mut last_error: Option<HandoffError> = None;
for attempt in 0..=max_retries {
if attempt > 0 {
let backoff_ms = self.config.retry.calculate_backoff(attempt - 1);
info!(
"Handoff retry: attempt={}/{}, backoff={}ms, specialist={}",
attempt, max_retries, backoff_ms, specialist_name
);
sleep(Duration::from_millis(backoff_ms)).await;
}
match executor.execute(specialist, task).await {
Ok(result) => {
info!(
"Handoff succeeded: specialist={}, tokens={}, loops={}, time={}ms",
specialist_name,
result.token_count,
result.loop_count,
result.execution_time_ms
);
record.set_result(result.output.clone());
return Ok((result.output, record));
}
Err(paladin_err) => {
let handoff_err = HandoffError::ExecutionFailed {
from_agent: from_agent.clone(),
to_agent: specialist_name.to_string(),
reason: paladin_err.to_string(),
};
if !Self::is_transient_error(&handoff_err) {
warn!(
"Handoff permanent failure: specialist={}, error={}",
specialist_name, handoff_err
);
return Err(handoff_err);
}
warn!(
"Handoff transient failure: specialist={}, attempt={}/{}, error={}",
specialist_name, attempt, max_retries, handoff_err
);
last_error = Some(handoff_err);
}
}
}
Err(last_error.unwrap_or_else(|| HandoffError::ExecutionFailed {
from_agent,
to_agent: specialist_name.to_string(),
reason: "All retry attempts exhausted".to_string(),
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strategy_getter() {
let config = Arc::new(HandoffConfig {
enabled: true,
strategy: HandoffStrategy::Explicit,
max_depth: 3,
retry: Default::default(),
});
let service = HandoffService::new(config).unwrap();
assert!(matches!(service.strategy(), HandoffStrategy::Explicit));
}
#[test]
fn test_max_depth_getter() {
let config = Arc::new(HandoffConfig {
enabled: true,
strategy: HandoffStrategy::Automatic,
max_depth: 7,
retry: Default::default(),
});
let service = HandoffService::new(config).unwrap();
assert_eq!(service.max_depth(), 7);
}
#[test]
fn test_threshold_strategy() {
let config = Arc::new(HandoffConfig {
enabled: true,
strategy: HandoffStrategy::threshold(0.85),
max_depth: 5,
retry: Default::default(),
});
let service = HandoffService::new(config).unwrap();
assert!(service.strategy().get_threshold().is_some());
assert!((service.strategy().get_threshold().unwrap() - 0.85).abs() < 0.01);
}
}