use crate::agent::Agent;
use crate::config::Config;
use crate::error::{HeliosError, Result};
use crate::tools::Tool;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentConfig {
pub name: String,
pub system_prompt: String,
#[serde(default)]
pub tool_indices: Vec<usize>,
pub role: String,
}
#[derive(Debug, Deserialize)]
struct OrchestrationPlanJson {
num_agents: usize,
reasoning: String,
agents: Vec<AgentConfig>,
task_breakdown: HashMap<String, String>,
}
pub struct SpawnedAgent {
pub agent: Agent,
pub config: AgentConfig,
pub result: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrchestrationPlan {
pub task: String,
pub num_agents: usize,
pub reasoning: String,
pub agents: Vec<AgentConfig>,
pub task_breakdown: HashMap<String, String>,
}
pub struct AutoForest {
config: Config,
tools: Vec<Box<dyn Tool>>,
spawned_agents: Vec<SpawnedAgent>,
orchestration_plan: Option<OrchestrationPlan>,
orchestrator_agent: Option<Agent>,
}
impl AutoForest {
#[allow(clippy::new_ret_no_self)]
pub fn new(config: Config) -> AutoForestBuilder {
AutoForestBuilder::new(config)
}
pub fn orchestration_plan(&self) -> Option<&OrchestrationPlan> {
self.orchestration_plan.as_ref()
}
pub fn spawned_agents(&self) -> &[SpawnedAgent] {
&self.spawned_agents
}
async fn generate_orchestration_plan(&mut self, task: &str) -> Result<OrchestrationPlan> {
let tools_info = self
.tools
.iter()
.enumerate()
.map(|(i, tool)| format!("- Tool {}: {} ({})", i, tool.name(), tool.description()))
.collect::<Vec<_>>()
.join("\n");
let orchestrator_prompt = format!(
r#"You are an expert task orchestrator. Your job is to analyze a task and create an optimal plan for a forest of AI agents to complete it.
Available tools:
{}
Given the task, you must:
1. Determine the optimal number of agents (1-5)
2. Define each agent's role and specialization
3. Create specialized system prompts for each agent
4. Assign tools to each agent based on their role
5. Break down the task into subtasks for each agent
Respond with ONLY a JSON object with this structure (no markdown, no extra text):
{{
"num_agents": <number>,
"reasoning": "<brief explanation>",
"agents": [
{{
"name": "<agent_name>",
"role": "<role>",
"system_prompt": "<specialized_prompt>",
"tool_indices": [<indices>]
}}
],
"task_breakdown": {{
"<agent_name>": "<specific_task_for_this_agent>"
}}
}}"#,
tools_info
);
if self.orchestrator_agent.is_none() {
let orchestrator = Agent::builder("Orchestrator")
.config(self.config.clone())
.system_prompt(&orchestrator_prompt)
.build()
.await?;
self.orchestrator_agent = Some(orchestrator);
}
let orchestrator = self.orchestrator_agent.as_mut().ok_or_else(|| {
HeliosError::AgentError("Failed to create orchestrator agent".to_string())
})?;
let response = orchestrator.chat(&format!("Task: {}", task)).await?;
let plan_data: OrchestrationPlanJson = serde_json::from_str(&response).map_err(|e| {
HeliosError::AgentError(format!("Failed to parse orchestration plan: {}", e))
})?;
let plan = OrchestrationPlan {
task: task.to_string(),
num_agents: plan_data.num_agents,
reasoning: plan_data.reasoning,
agents: plan_data.agents,
task_breakdown: plan_data.task_breakdown,
};
self.orchestration_plan = Some(plan.clone());
Ok(plan)
}
async fn spawn_agents_from_plan(&mut self, plan: &OrchestrationPlan) -> Result<()> {
self.spawned_agents.clear();
for agent_config in &plan.agents {
let agent = Agent::builder(&agent_config.name)
.config(self.config.clone())
.system_prompt(&agent_config.system_prompt)
.build()
.await?;
let spawned = SpawnedAgent {
agent,
config: agent_config.clone(),
result: None,
};
self.spawned_agents.push(spawned);
}
Ok(())
}
pub async fn execute_task(&mut self, task: &str) -> Result<String> {
let plan = self.generate_orchestration_plan(task).await?;
self.spawn_agents_from_plan(&plan).await?;
let mut futures = Vec::new();
for spawned_agent in self.spawned_agents.drain(..) {
let agent_task = plan
.task_breakdown
.get(&spawned_agent.config.name)
.cloned()
.unwrap_or_else(|| format!("Complete your assigned portion of: {}", task));
let future = async move {
let mut agent = spawned_agent.agent;
let config = spawned_agent.config;
let result = agent.chat(&agent_task).await;
(agent, config, result)
};
futures.push(future);
}
let completed_agents = futures::future::join_all(futures).await;
let mut results = HashMap::new();
self.spawned_agents.clear();
for (agent, config, result) in completed_agents {
let agent_name = config.name.clone();
let (result_string, result_for_map) = match result {
Ok(output) => (Some(output.clone()), output),
Err(e) => {
let err_msg = format!("Error: {}", e);
(Some(err_msg.clone()), err_msg)
}
};
results.insert(agent_name, result_for_map);
self.spawned_agents.push(SpawnedAgent {
agent,
config,
result: result_string,
});
}
let aggregated_result = self.aggregate_results(&results, task).await?;
Ok(aggregated_result)
}
pub async fn do_task(&mut self, task: &str) -> Result<String> {
self.execute_task(task).await
}
pub async fn run(&mut self, task: &str) -> Result<String> {
self.execute_task(task).await
}
async fn aggregate_results(
&mut self,
results: &HashMap<String, String>,
task: &str,
) -> Result<String> {
let mut result_text = String::new();
result_text.push_str("## Task Execution Summary\n\n");
result_text.push_str(&format!("**Task**: {}\n\n", task));
result_text.push_str("### Agent Results:\n\n");
for (agent_name, result) in results {
result_text.push_str(&format!("**{}**:\n{}\n\n", agent_name, result));
}
if results.len() > 1 {
result_text.push_str("### Synthesized Analysis:\n\n");
let orchestrator = self
.orchestrator_agent
.as_mut()
.ok_or_else(|| HeliosError::AgentError("Orchestrator not available".to_string()))?;
let synthesis_prompt = format!(
"Synthesize these agent results into a cohesive answer:\n{}",
result_text
);
let synthesis = orchestrator.chat(&synthesis_prompt).await?;
result_text.push_str(&synthesis);
}
Ok(result_text)
}
}
pub struct AutoForestBuilder {
config: Config,
tools: Vec<Box<dyn Tool>>,
}
impl AutoForestBuilder {
pub fn new(config: Config) -> Self {
Self {
config,
tools: Vec::new(),
}
}
pub fn with_tools(mut self, tools: Vec<Box<dyn Tool>>) -> Self {
self.tools = tools;
self
}
pub async fn build(self) -> Result<AutoForest> {
Ok(AutoForest {
config: self.config,
tools: self.tools,
spawned_agents: Vec::new(),
orchestration_plan: None,
orchestrator_agent: None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_agent_config_creation() {
let config = AgentConfig {
name: "TestAgent".to_string(),
system_prompt: "You are helpful".to_string(),
tool_indices: vec![0, 1],
role: "Analyzer".to_string(),
};
assert_eq!(config.name, "TestAgent");
assert_eq!(config.tool_indices.len(), 2);
}
#[test]
fn test_orchestration_plan_creation() {
let plan = OrchestrationPlan {
task: "Test task".to_string(),
num_agents: 2,
reasoning: "Two agents needed".to_string(),
agents: vec![],
task_breakdown: HashMap::new(),
};
assert_eq!(plan.num_agents, 2);
assert_eq!(plan.task, "Test task");
}
}