//! Agent spawning tools — launch subagents and pipelines. //! //! `SpawnAgents` runs multiple subagents in parallel threads. `SpawnPipeline` //! runs a sequence of agent stages one after another. use anyhow::Result; use serde_json::{json, Value}; use tracing::{debug, info, instrument, warn}; use zesdex_domain::cms::{AppConfigRepository, SettingsRepository}; use zesdex_domain::core::Store; use crate::persistence::{JsonAppConfigRepository, JsonSettingsRepository}; use crate::subagent::context::SubagentContext; use crate::subagent::division::AccessTier; use crate::subagent::engine::run_agent; use crate::subagent::spawn::spawn_subagent; use crate::tools::{Tool, ToolCtx}; /// Spawn multiple agent instances to work in parallel on subtasks. /// /// Flow: parse agents array → load settings → for each agent, build a /// SubagentContext and call spawn_subagent → join all threads → collect results. pub struct SpawnAgents; impl Tool for SpawnAgents { fn name(&self) -> &'static str { "spawn_agents" } fn description(&self) -> &'static str { "Spawn multiple agent instances to work in parallel on subtasks" } fn parameters(&self) -> Value { json!({ "type": "object", "properties": { "agents": { "type": "array", "items": { "type": "object", "properties": { "directive": {"type": "string", "description": "Directive for the agent"}, "access": {"type": "string", "enum": ["read", "write", "full"], "description": "Access tier"} }, "required": ["directive"] }, "description": "List of agents to spawn" } }, "required": ["agents"] }) } #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let agents = args .get("agents") .and_then(|v| v.as_array()) .ok_or_else(|| anyhow::anyhow!("missing 'agents' array"))?; info!("Spawning {} agents", agents.len()); debug!(agent_count = agents.len(), "parsing agents array"); // Load LLM credentials once for all agents let store = Store::new(); let settings = JsonSettingsRepository::new() .load(&store.base_dir) .unwrap_or_default(); let app_config = JsonAppConfigRepository::new() .load(&store.base_dir) .unwrap_or_default(); let (provider, model) = crate::subagent::provider::resolve_subagent_provider(&settings, &app_config); let base_url = app_config .providers .get(&provider) .map(|p| p.api_base.clone()) .unwrap_or_else(|| zesdex_domain::agent::defaults::DEFAULT_API_BASE.to_string()); let api_key = crate::llm::provider::resolve_api_key(&settings, &app_config); let mut handles = Vec::new(); for (i, agent) in agents.iter().enumerate() { let directive = agent .get("directive") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); let access_str = agent .get("access") .and_then(|v| v.as_str()) .unwrap_or("full"); let access = match access_str { "read" => AccessTier::Read, "write" => AccessTier::Write, _ => AccessTier::Full, }; let subagent_ctx = SubagentContext::new( directive.clone(), ctx.clone(), access_str.to_string(), base_url.clone(), api_key.clone(), model.clone(), ); debug!(agent_index = i, access = %access_str, "spawning subagent"); let handle = spawn_subagent(subagent_ctx, directive.clone(), access, ctx.clone()); handles.push((i, handle)); } // Join all handles and collect results let mut results = Vec::new(); for (i, handle) in handles { let result = handle .join() .map_err(|e| anyhow::anyhow!("subagent {i} panicked: {e:?}"))??; results.push(format!("Agent {i}: {result}")); info!(agent_index = i, "subagent completed"); } info!("All {} subagents completed", agents.len()); Ok(format!( "Spawned {} agents.\n\nResults:\n{}", agents.len(), results.join("\n") )) } } /// Spawn a sequential pipeline of agent stages. /// /// Flow: parse stages → load settings → for each stage, build a /// SubagentContext and call run_agent sequentially → collect results. pub struct SpawnPipeline; impl Tool for SpawnPipeline { fn name(&self) -> &'static str { "spawn_pipeline" } fn description(&self) -> &'static str { "Spawn a sequential pipeline of agent stages" } fn parameters(&self) -> Value { json!({ "type": "object", "properties": { "stages": { "type": "array", "items": { "type": "object", "properties": { "directive": {"type": "string", "description": "Directive for this pipeline stage"}, "access": {"type": "string", "enum": ["read", "write", "full"], "description": "Access tier for this stage"} }, "required": ["directive"] }, "description": "Pipeline stages in order" } }, "required": ["stages"] }) } #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let stages = args .get("stages") .and_then(|v| v.as_array()) .ok_or_else(|| anyhow::anyhow!("missing 'stages' array"))?; info!("Spawning pipeline with {} stages", stages.len()); // Load LLM credentials once for all stages let store = Store::new(); let settings = JsonSettingsRepository::new() .load(&store.base_dir) .unwrap_or_default(); let app_config = JsonAppConfigRepository::new() .load(&store.base_dir) .unwrap_or_default(); let (provider, model) = crate::subagent::provider::resolve_subagent_provider(&settings, &app_config); let base_url = app_config .providers .get(&provider) .map(|p| p.api_base.clone()) .unwrap_or_else(|| zesdex_domain::agent::defaults::DEFAULT_API_BASE.to_string()); let api_key = crate::llm::provider::resolve_api_key(&settings, &app_config); let rt = tokio::runtime::Runtime::new()?; let mut pipeline_result = String::new(); for (i, stage) in stages.iter().enumerate() { let directive = stage .get("directive") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); let access_str = stage .get("access") .and_then(|v| v.as_str()) .unwrap_or("full"); let access = match access_str { "read" => AccessTier::Read, "write" => AccessTier::Write, _ => AccessTier::Full, }; let subagent_ctx = SubagentContext::new( directive.clone(), ctx.clone(), access_str.to_string(), base_url.clone(), api_key.clone(), model.clone(), ); debug!(stage_index = i, access = %access_str, "running pipeline stage"); let result = rt.block_on(async { run_agent(subagent_ctx, &directive, access, ctx.clone()).await })?; pipeline_result.push_str(&format!("Stage {}: {}\n", i, result)); info!(stage_index = i, "pipeline stage completed"); } info!("Pipeline with {} stages completed", stages.len()); Ok(format!( "Pipeline with {} stages completed.\n\n{}", stages.len(), pipeline_result )) } }