Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Batch 0: - TRUTH.md 中间件层 14→15 (补 EvolutionMiddleware@78) - wiki/middleware.md 同步 15 层 + 优先级分类更新 - Store 数字确认 25 个 Batch 1: - approvals.rs: 3 处 map_err+let _ = 简化为 if let Err - director.rs: oneshot send 失败添加 debug 日志 - task.rs: 4 处子任务状态更新添加 debug 日志 - chat.rs: 流消息发送和事件 emit 添加 warn/debug 日志 - heartbeat.rs: 告警广播添加 debug 日志 + break 优化 全量测试通过: 719 passed, 0 failed
234 lines
8.3 KiB
Rust
234 lines
8.3 KiB
Rust
//! 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<dyn LlmDriver>,
|
||
memory: Arc<MemoryStore>,
|
||
model: String,
|
||
max_tokens: u32,
|
||
temperature: f32,
|
||
}
|
||
|
||
impl TaskTool {
|
||
pub fn new(
|
||
driver: Arc<dyn LlmDriver>,
|
||
memory: Arc<MemoryStore>,
|
||
model: impl Into<String>,
|
||
) -> 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<Value> {
|
||
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)
|
||
}
|
||
}
|