2026-07-20 09:04:57 +07:00
|
|
|
//! Agent spawning tools — launch subagents and pipelines.
|
2026-07-20 15:53:20 +07:00
|
|
|
//!
|
|
|
|
|
//! `SpawnAgents` runs multiple subagents in parallel threads. `SpawnPipeline`
|
|
|
|
|
//! runs a sequence of agent stages one after another.
|
2026-07-20 09:04:57 +07:00
|
|
|
|
|
|
|
|
use anyhow::Result;
|
|
|
|
|
use serde_json::{json, Value};
|
2026-07-20 15:53:20 +07:00
|
|
|
use tracing::{debug, info, instrument, warn};
|
2026-07-20 09:04:57 +07:00
|
|
|
|
|
|
|
|
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"]
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 15:53:20 +07:00
|
|
|
#[instrument(skip(self, ctx, args))]
|
2026-07-20 09:04:57 +07:00
|
|
|
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
|
|
|
|
let agents = args
|
|
|
|
|
.get("agents")
|
|
|
|
|
.and_then(|v| v.as_array())
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("missing 'agents' array"))?;
|
|
|
|
|
|
|
|
|
|
info!("Spawning {} agents", agents.len());
|
2026-07-20 15:53:20 +07:00
|
|
|
debug!(agent_count = agents.len(), "parsing agents array");
|
2026-07-20 09:04:57 +07:00
|
|
|
|
|
|
|
|
// 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(|| "https://opencode.ai/zen/v1".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(),
|
|
|
|
|
);
|
|
|
|
|
|
2026-07-20 15:53:20 +07:00
|
|
|
debug!(agent_index = i, access = %access_str, "spawning subagent");
|
2026-07-20 09:04:57 +07:00
|
|
|
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}"));
|
2026-07-20 15:53:20 +07:00
|
|
|
info!(agent_index = i, "subagent completed");
|
2026-07-20 09:04:57 +07:00
|
|
|
}
|
|
|
|
|
|
2026-07-20 15:53:20 +07:00
|
|
|
info!("All {} subagents completed", agents.len());
|
2026-07-20 09:04:57 +07:00
|
|
|
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": {
|
2026-07-20 12:26:08 +07:00
|
|
|
"directive": {"type": "string", "description": "Directive for this pipeline stage"},
|
|
|
|
|
"access": {"type": "string", "enum": ["read", "write", "full"], "description": "Access tier for this stage"}
|
2026-07-20 09:04:57 +07:00
|
|
|
},
|
|
|
|
|
"required": ["directive"]
|
|
|
|
|
},
|
|
|
|
|
"description": "Pipeline stages in order"
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
"required": ["stages"]
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 15:53:20 +07:00
|
|
|
#[instrument(skip(self, ctx, args))]
|
2026-07-20 09:04:57 +07:00
|
|
|
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
|
|
|
|
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(|| "https://opencode.ai/zen/v1".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();
|
|
|
|
|
|
2026-07-20 12:26:08 +07:00
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-20 09:04:57 +07:00
|
|
|
let subagent_ctx = SubagentContext::new(
|
|
|
|
|
directive.clone(),
|
|
|
|
|
ctx.clone(),
|
2026-07-20 12:26:08 +07:00
|
|
|
access_str.to_string(),
|
2026-07-20 09:04:57 +07:00
|
|
|
base_url.clone(),
|
|
|
|
|
api_key.clone(),
|
|
|
|
|
model.clone(),
|
|
|
|
|
);
|
|
|
|
|
|
2026-07-20 15:53:20 +07:00
|
|
|
debug!(stage_index = i, access = %access_str, "running pipeline stage");
|
2026-07-20 09:04:57 +07:00
|
|
|
let result = rt.block_on(async {
|
2026-07-20 12:26:08 +07:00
|
|
|
run_agent(subagent_ctx, &directive, access, ctx.clone()).await
|
2026-07-20 09:04:57 +07:00
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
pipeline_result.push_str(&format!("Stage {}: {}\n", i, result));
|
2026-07-20 15:53:20 +07:00
|
|
|
info!(stage_index = i, "pipeline stage completed");
|
2026-07-20 09:04:57 +07:00
|
|
|
}
|
|
|
|
|
|
2026-07-20 15:53:20 +07:00
|
|
|
info!("Pipeline with {} stages completed", stages.len());
|
2026-07-20 09:04:57 +07:00
|
|
|
Ok(format!(
|
|
|
|
|
"Pipeline with {} stages completed.\n\n{}",
|
|
|
|
|
stages.len(),
|
|
|
|
|
pipeline_result
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
}
|