//! Task tool — delegates sub-tasks to a nested AgentLoop. //! //! Inspired by DeerFlow's `task_tool`: the lead agent can spawn sub-agent tasks //! to parallelise complex work. Each sub-task runs its own AgentLoop with a //! fresh session, isolated context, and a configurable maximum iteration count. use async_trait::async_trait; use serde_json::{json, Value}; use zclaw_types::{AgentId, Result, ZclawError}; use zclaw_memory::MemoryStore; use crate::driver::LlmDriver; use crate::loop_runner::{AgentLoop, LoopEvent}; use crate::tool::{Tool, ToolContext, ToolRegistry}; use crate::tool::builtin::register_builtin_tools; use std::sync::Arc; /// Default max iterations for a sub-agent task. const DEFAULT_MAX_ITERATIONS: usize = 5; /// Tool that delegates sub-tasks to a nested AgentLoop. pub struct TaskTool { driver: Arc, memory: Arc, model: String, max_tokens: u32, temperature: f32, } impl TaskTool { pub fn new( driver: Arc, memory: Arc, model: impl Into, ) -> Self { Self { driver, memory, model: model.into(), max_tokens: 4096, temperature: 0.7, } } pub fn with_max_tokens(mut self, max_tokens: u32) -> Self { self.max_tokens = max_tokens; self } pub fn with_temperature(mut self, temperature: f32) -> Self { self.temperature = temperature; self } } #[async_trait] impl Tool for TaskTool { fn name(&self) -> &str { "task" } fn description(&self) -> &str { "Delegate a sub-task to a sub-agent. The sub-agent will work independently \ with its own context and tools. Use this to break complex tasks into \ parallel or sequential sub-tasks. Each sub-task runs in its own session \ with a focused system prompt." } fn input_schema(&self) -> Value { json!({ "type": "object", "properties": { "description": { "type": "string", "description": "Short description of the sub-task (shown in progress UI)" }, "prompt": { "type": "string", "description": "Detailed instructions for the sub-agent" }, "max_iterations": { "type": "integer", "description": "Maximum tool-call iterations for the sub-agent (default: 5)", "minimum": 1, "maximum": 10 } }, "required": ["description", "prompt"] }) } async fn execute(&self, input: Value, context: &ToolContext) -> Result { let description = input["description"].as_str() .ok_or_else(|| ZclawError::InvalidInput("Missing 'description' parameter".into()))?; let prompt = input["prompt"].as_str() .ok_or_else(|| ZclawError::InvalidInput("Missing 'prompt' parameter".into()))?; let max_iterations = input["max_iterations"].as_u64() .unwrap_or(DEFAULT_MAX_ITERATIONS as u64) as usize; tracing::info!( "[TaskTool] Starting sub-agent task: {:?} (max_iterations={})", description, max_iterations ); // Emit subtask_started event // Create a sub-agent with its own ID let sub_agent_id = AgentId::new(); let task_id = sub_agent_id.to_string(); if let Some(ref tx) = context.event_sender { if tx.send(LoopEvent::SubtaskStatus { task_id: task_id.clone(), description: description.to_string(), status: "started".to_string(), detail: None, }).await.is_err() { tracing::debug!("[TaskTool] Subtask status dropped: parent loop ended"); } } // Create a fresh session for the sub-agent let session_id = self.memory.create_session(&sub_agent_id).await?; // Build system prompt focused on the sub-task let system_prompt = format!( "你是一个专注的子Agent,负责完成以下任务:{}\n\n\ 要求:\n\ - 专注完成分配给你的任务\n\ - 使用可用的工具来完成任务\n\ - 完成后提供简洁的结果摘要\n\ - 如果遇到无法解决的问题,请说明原因", description ); // Create a tool registry with builtin tools // (TaskTool itself is NOT included to prevent infinite nesting) let mut tools = ToolRegistry::new(); register_builtin_tools(&mut tools); // Build a lightweight AgentLoop for the sub-agent let mut sub_loop = AgentLoop::new( sub_agent_id, self.driver.clone(), tools, self.memory.clone(), ) .with_model(&self.model) .with_system_prompt(&system_prompt) .with_max_tokens(self.max_tokens) .with_temperature(self.temperature); // Optionally inject skill executor and path validator from parent context if let Some(ref executor) = context.skill_executor { sub_loop = sub_loop.with_skill_executor(executor.clone()); } if let Some(ref validator) = context.path_validator { sub_loop = sub_loop.with_path_validator(validator.clone()); } // Emit subtask_running event if let Some(ref tx) = context.event_sender { if tx.send(LoopEvent::SubtaskStatus { task_id: task_id.clone(), description: description.to_string(), status: "running".to_string(), detail: Some("子Agent正在执行中...".to_string()), }).await.is_err() { tracing::debug!("[TaskTool] Subtask status dropped: parent loop ended"); } } // Execute the sub-agent loop (non-streaming — collect full result) let result = match sub_loop.run(session_id.clone(), prompt.to_string()).await { Ok(loop_result) => { tracing::info!( "[TaskTool] Sub-agent completed: {} iterations, {} input tokens, {} output tokens", loop_result.iterations, loop_result.input_tokens, loop_result.output_tokens ); // Emit subtask_completed event if let Some(ref tx) = context.event_sender { if tx.send(LoopEvent::SubtaskStatus { task_id: task_id.clone(), description: description.to_string(), status: "completed".to_string(), detail: Some(format!( "完成 ({}次迭代, {}输入token)", loop_result.iterations, loop_result.input_tokens )), }).await.is_err() { tracing::debug!("[TaskTool] Subtask status dropped: parent loop ended"); } } json!({ "status": "completed", "description": description, "result": loop_result.response, "iterations": loop_result.iterations, "input_tokens": loop_result.input_tokens, "output_tokens": loop_result.output_tokens, }) } Err(e) => { tracing::warn!("[TaskTool] Sub-agent failed: {}", e); // Emit subtask_failed event if let Some(ref tx) = context.event_sender { if tx.send(LoopEvent::SubtaskStatus { task_id: task_id.clone(), description: description.to_string(), status: "failed".to_string(), detail: Some(e.to_string()), }).await.is_err() { tracing::debug!("[TaskTool] Subtask status dropped: parent loop ended"); } } json!({ "status": "failed", "description": description, "error": e.to_string(), }) } }; Ok(result) } }