use rig::agent::AgentBuilder; use rig::client::CompletionClient; use rig::completion::Prompt; use tracing::{debug, warn}; use super::config::AgentConfig; use super::helpers::with_retry; use super::persistence::{ ActiveAgentRun, AgentRealtime, AgentRuntime, AgentStreamEvent, estimate_output_tokens, }; use super::request::{AgentExpert, AgentExpertOutput}; use crate::client::AiClient; use crate::error::{AiError, AiResult}; pub async fn run_experts( client: &AiClient, config: &AgentConfig, experts: &[AgentExpert], realtime: Option<&AgentRealtime>, run: &ActiveAgentRun, ) -> AiResult> { let mut outputs = Vec::with_capacity(experts.len()); let mut failed_count = 0; for expert in experts { match run_single(client, config, expert, realtime, run).await { Ok(output) => { debug!(subagent_id = %output.id, role = %output.role, "subagent completed"); outputs.push(output); } Err(error) => { warn!(subagent_id = %expert.id, role = %expert.role, error = %error, "subagent failed"); let _ = publish_subagent_failed(realtime, run, expert, &error.to_string()).await; failed_count += 1; } } } debug!(total = experts.len(), ok = outputs.len(), failed = failed_count, "experts done"); Ok(outputs) } async fn run_single( client: &AiClient, config: &AgentConfig, expert: &AgentExpert, realtime: Option<&AgentRealtime>, run: &ActiveAgentRun, ) -> AiResult { publish_subagent_started(realtime, run, config, expert).await?; let rig_client = client.llm_client().clone(); let model_name = config.model.clone(); let temperature = expert.temperature.or(config.temperature); let max_completion_tokens = expert.max_completion_tokens.or(config.max_completion_tokens); let retry_attempts = config.retry_max_attempts; let retry_delay_ms = config.retry_base_delay_ms; let prompt = expert.system_prompt.clone().unwrap_or_else(|| { format!( "You are a specialist subagent. Role: {}. Produce a concise expert answer for the parent chat agent.", expert.role ) }); let task = build_expert_task(expert); let (output, input_tokens_usage, output_tokens_usage) = with_retry( retry_attempts, retry_delay_ms, || { let rig_client = rig_client.clone(); let model_name = model_name.clone(); let prompt = prompt.clone(); let task = task.clone(); async move { let model = rig_client.completion_model(&model_name); let mut builder = AgentBuilder::new(model).preamble(&prompt); if let Some(temp) = temperature { builder = builder.temperature(temp); } if let Some(mt) = max_completion_tokens { builder = builder.max_tokens(mt); } let agent = builder.build(); let response = agent .prompt(&task) .extended_details() .await .map_err(|e: rig::completion::PromptError| { AiError::Api(e.to_string()) })?; Ok(( response.output, response.usage.input_tokens, response.usage.output_tokens, )) } }, ) .await?; let input_tokens = input_tokens_usage as i64; let output_tokens = if output_tokens_usage > 0 { output_tokens_usage as i64 } else { estimate_output_tokens(&output) }; let result = AgentExpertOutput { id: expert.id.clone(), role: expert.role.clone(), task: expert.task.clone(), output, input_tokens, output_tokens, }; publish_subagent_completed(realtime, run, config, &result).await?; Ok(result) } fn build_expert_task(expert: &AgentExpert) -> String { let mut task = String::new(); if !expert.context.is_empty() { task.push_str("Retrieved context for this specialist task:\n"); for (index, chunk) in expert.context.iter().enumerate() { task.push_str(&format!( "\n[{}] id={} source={}\n{}\n", index + 1, chunk.id, chunk.source.as_deref().unwrap_or("unknown"), chunk.content )); } task.push('\n'); } task.push_str(&expert.task); task } async fn publish_subagent_started( realtime: Option<&AgentRealtime>, run: &ActiveAgentRun, config: &AgentConfig, expert: &AgentExpert, ) -> AiResult<()> { AgentRuntime::default().publish( realtime, &AgentStreamEvent::SubagentStarted { conversation_id: run.conversation_id, message_id: run.message_id, subagent_id: expert.id.clone(), role: expert.role.clone(), task: expert.task.clone(), model: config.model.clone(), }, ).await } async fn publish_subagent_completed( realtime: Option<&AgentRealtime>, run: &ActiveAgentRun, config: &AgentConfig, output: &AgentExpertOutput, ) -> AiResult<()> { AgentRuntime::default().publish( realtime, &AgentStreamEvent::SubagentCompleted { conversation_id: run.conversation_id, message_id: run.message_id, subagent_id: output.id.clone(), role: output.role.clone(), task: output.task.clone(), output: output.output.clone(), input_tokens: output.input_tokens, output_tokens: output.output_tokens, model: config.model.clone(), }, ).await } async fn publish_subagent_failed( realtime: Option<&AgentRealtime>, run: &ActiveAgentRun, expert: &AgentExpert, error: &str, ) -> AiResult<()> { AgentRuntime::default().publish( realtime, &AgentStreamEvent::SubagentFailed { conversation_id: run.conversation_id, message_id: run.message_id, subagent_id: expert.id.clone(), error: error.to_string(), }, ).await }