mod tool;
use crate::agent::Agent;
use crate::error::{AgentError, ReactError, Result};
use crate::llm::types::Message;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
use tracing::{debug, info};
pub use tool::HandoffTool;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HandoffTarget {
pub agent_name: String,
pub message: Option<String>,
pub transfer_history: bool,
}
impl HandoffTarget {
pub fn new(agent_name: impl Into<String>) -> Self {
Self {
agent_name: agent_name.into(),
message: None,
transfer_history: false,
}
}
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = Some(message.into());
self
}
pub fn with_history(mut self) -> Self {
self.transfer_history = true;
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct HandoffContext {
pub source_agent: Option<String>,
pub messages: Vec<Message>,
pub metadata: HashMap<String, String>,
}
impl HandoffContext {
pub fn new() -> Self {
Self::default()
}
pub fn with_source(mut self, source: impl Into<String>) -> Self {
self.source_agent = Some(source.into());
self
}
pub fn with_messages(mut self, messages: Vec<Message>) -> Self {
self.messages = messages;
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HandoffResult {
pub target_agent: String,
pub source_agent: Option<String>,
pub output: String,
pub return_to_source: bool,
}
pub struct HandoffManager {
agents: HashMap<String, Arc<Mutex<Box<dyn Agent>>>>,
}
impl HandoffManager {
pub fn new() -> Self {
Self {
agents: HashMap::new(),
}
}
pub fn register(&mut self, name: impl Into<String>, agent: impl Agent + 'static) {
let name = name.into();
self.agents
.insert(name, Arc::new(Mutex::new(Box::new(agent))));
}
pub fn register_boxed(&mut self, name: impl Into<String>, agent: Box<dyn Agent>) {
let name = name.into();
self.agents.insert(name, Arc::new(Mutex::new(agent)));
}
pub fn register_shared(&mut self, name: impl Into<String>, agent: Arc<Mutex<Box<dyn Agent>>>) {
self.agents.insert(name.into(), agent);
}
pub fn registered_agents(&self) -> Vec<&str> {
self.agents.keys().map(|s| s.as_str()).collect()
}
pub fn has_agent(&self, name: &str) -> bool {
self.agents.contains_key(name)
}
pub async fn handoff(
&self,
target: HandoffTarget,
context: HandoffContext,
) -> Result<HandoffResult> {
let agent_arc = self.agents.get(&target.agent_name).ok_or_else(|| {
ReactError::Agent(AgentError::InitializationFailed(format!(
"Handoff target agent '{}' is not registered. Available agents: {:?}",
target.agent_name,
self.registered_agents()
)))
})?;
info!(
source = ?context.source_agent,
target = %target.agent_name,
transfer_history = %target.transfer_history,
metadata_keys = ?context.metadata.keys().collect::<Vec<_>>(),
"🤝 Executing handoff"
);
let full_prompt = {
let mut prompt_parts = Vec::new();
if let Some(source) = &context.source_agent {
prompt_parts.push(format!("[Handoff source: Agent '{}']", source));
}
if !context.metadata.is_empty() {
let meta_lines: Vec<String> = context
.metadata
.iter()
.map(|(k, v)| format!(" - {}: {}", k, v))
.collect();
prompt_parts.push(format!("[Context metadata]\n{}", meta_lines.join("\n")));
}
if target.transfer_history && !context.messages.is_empty() {
let history_summary: Vec<String> = context
.messages
.iter()
.filter_map(|msg| {
msg.content
.as_text_ref()
.map(|c| format!("{}: {}", msg.role, c))
})
.collect();
prompt_parts.push(format!(
"[Conversation history]\n{}",
history_summary.join("\n")
));
}
if let Some(message) = &target.message {
prompt_parts.push(format!("[Task]\n{}", message));
}
prompt_parts.join("\n\n")
};
debug!(
target = %target.agent_name,
prompt_len = full_prompt.len(),
"📨 Sending handoff prompt"
);
let agent_arc_clone = agent_arc.clone();
let (tx, rx) = tokio::sync::oneshot::channel();
tokio::spawn(async move {
let agent = agent_arc_clone.lock().await;
let result = agent.execute(&full_prompt).await;
let _ = tx.send(result);
});
let output = rx
.await
.map_err(|_| ReactError::Other("Handoff task failed to complete".to_string()))??;
info!(
target = %target.agent_name,
output_len = output.len(),
"Handoff execution completed"
);
Ok(HandoffResult {
target_agent: target.agent_name,
source_agent: context.source_agent,
output,
return_to_source: false,
})
}
pub async fn handoff_chain(
&self,
targets: Vec<HandoffTarget>,
initial_context: HandoffContext,
) -> Result<Vec<HandoffResult>> {
let mut results = Vec::new();
let mut current_context = initial_context;
for target in targets {
let mut target_with_history = target.clone();
target_with_history.transfer_history = true;
let result = self
.handoff(target_with_history, current_context.clone())
.await?;
let mut updated_messages = current_context.messages.clone();
if let Some(task_msg) = &target.message {
updated_messages.push(Message::user(task_msg.clone()));
}
updated_messages.push(Message::assistant(result.output.clone()));
current_context = HandoffContext {
source_agent: Some(result.target_agent.clone()),
messages: updated_messages,
metadata: {
let mut metadata = current_context.metadata.clone();
metadata.insert("previous_output".to_string(), result.output.clone());
metadata
},
};
results.push(result);
}
Ok(results)
}
}
impl Default for HandoffManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_handoff_target() {
let target = HandoffTarget::new("agent_b")
.with_message("Please process this task")
.with_history();
assert_eq!(target.agent_name, "agent_b");
assert_eq!(target.message.as_deref(), Some("Please process this task"));
assert!(target.transfer_history);
}
#[test]
fn test_handoff_context() {
let ctx = HandoffContext::new()
.with_source("agent_a")
.with_metadata("key1", "value1")
.with_metadata("key2", "value2");
assert_eq!(ctx.source_agent.as_deref(), Some("agent_a"));
assert_eq!(ctx.metadata.len(), 2);
assert_eq!(ctx.metadata.get("key1").unwrap(), "value1");
}
#[test]
fn test_handoff_manager_register() {
let manager = HandoffManager::new();
assert!(manager.registered_agents().is_empty());
assert!(!manager.has_agent("test"));
}
#[tokio::test]
async fn test_handoff_agent_not_found() {
let manager = HandoffManager::new();
let target = HandoffTarget::new("nonexistent");
let ctx = HandoffContext::new();
let result = manager.handoff(target, ctx).await;
assert!(result.is_err());
}
}