1use crate::agent::{AgentLoader, AgentMode, AgentRegistry};
2use crate::config::Settings;
3use crate::core::agent::subagent_manager::{SubagentManager, SubagentRequest};
4use crate::session::SessionStore;
5use crate::tool::{Tool, ToolResult, ToolSchema, parse_tool_args};
6use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use serde_json::json;
9use std::path::PathBuf;
10use std::sync::Arc;
11
12#[derive(Clone)]
13pub struct TaskToolRuntimeContext {
14 pub manager: Arc<SubagentManager>,
15 pub settings: Settings,
16 pub workspace_root: PathBuf,
17 pub parent_session_id: String,
18 pub parent_task_id: Option<String>,
19 pub depth: usize,
20}
21
22pub struct TaskTool {
23 context: TaskToolRuntimeContext,
24 available_subagents: Vec<AvailableSubagent>,
25}
26
27impl TaskTool {
28 pub fn new(context: TaskToolRuntimeContext) -> Self {
29 let available_subagents = discover_available_subagents();
30 Self {
31 context,
32 available_subagents,
33 }
34 }
35}
36
37#[derive(Debug, Clone)]
38struct AvailableSubagent {
39 name: String,
40 description: String,
41}
42
43#[derive(Debug, Serialize)]
44struct TaskToolOutput {
45 task_id: String,
46 name: String,
47 description: String,
48 status: String,
49 message: String,
50 agent_name: String,
51 prompt: String,
52 depth: usize,
53 parent_task_id: Option<String>,
54 started_at: u64,
55 finished_at: Option<u64>,
56 summary: Option<String>,
57 error: Option<String>,
58}
59
60#[derive(Debug, Deserialize)]
61struct TaskToolArgs {
62 name: String,
63 description: String,
64 prompt: String,
65 subagent_type: String,
66 #[serde(default)]
67 task_id: Option<String>,
68}
69
70#[async_trait]
71impl Tool for TaskTool {
72 fn schema(&self) -> ToolSchema {
73 let subagent_names: Vec<String> = self
74 .available_subagents
75 .iter()
76 .map(|agent| agent.name.clone())
77 .collect();
78 let mut subagent_type_schema = json!({
79 "type": "string",
80 "description": "Registered sub-agent name"
81 });
82 if !subagent_names.is_empty() {
83 subagent_type_schema["enum"] = json!(subagent_names);
84 }
85
86 ToolSchema {
87 name: "task".to_string(),
88 description: format!(
89 "Spawn or resume a sub-agent task.\n\nParameter contract:\n- `name` (required): human-readable task label shown in UI.\n- `description` (required): short statement of delegated intent.\n- `prompt` (required): full instructions for the child agent.\n- `subagent_type` (required): which registered sub-agent to run.\n- `task_id` (optional): if provided, resume that existing child task in this parent session; if omitted, create a new task.\n\nReturn semantics:\n- Returns terminal status for this task (`done`/`error`/`cancelled`) after execution completes.\n\n{}",
90 format_available_subagents(&self.available_subagents),
91 ),
92 capability: Some("task".to_string()),
93 mutating: Some(false),
94 parameters: json!({
95 "type": "object",
96 "properties": {
97 "name": {
98 "type": "string",
99 "description": "Required. Human-readable task name shown in UI subagent list"
100 },
101 "description": {
102 "type": "string",
103 "description": "Required. Short summary of what this delegated task should achieve"
104 },
105 "prompt": {
106 "type": "string",
107 "description": "Required. Full prompt/instructions executed by the selected sub-agent. Ask the child to keep output concise (short summary with only essential facts)."
108 },
109 "subagent_type": subagent_type_schema,
110 "task_id": {
111 "type": "string",
112 "description": "Optional. Existing task id to resume within the current parent session; omit to start a new task"
113 }
114 },
115 "required": ["name", "description", "prompt", "subagent_type"],
116 "additionalProperties": false
117 }),
118 }
119 }
120
121 async fn execute(&self, args: serde_json::Value) -> ToolResult {
122 let parsed: TaskToolArgs = match parse_tool_args(args, "task") {
123 Ok(value) => value,
124 Err(err) => return err,
125 };
126
127 if let Err(err) = validate_non_empty("name", &parsed.name) {
128 return err;
129 }
130 if let Err(err) = validate_non_empty("description", &parsed.description) {
131 return err;
132 }
133 if let Err(err) = validate_non_empty("prompt", &parsed.prompt) {
134 return err;
135 }
136
137 let registry = match load_agent_registry() {
138 Ok(registry) => registry,
139 Err(err) => return ToolResult::error(err),
140 };
141
142 let Some(agent) = registry.get_agent(&parsed.subagent_type) else {
143 return ToolResult::error(format!("unknown subagent_type: {}", parsed.subagent_type));
144 };
145 if agent.mode != AgentMode::Subagent {
146 return ToolResult::error(format!(
147 "agent '{}' is not a subagent (mode is {:?})",
148 agent.name, agent.mode
149 ));
150 }
151
152 let parent_session = match SessionStore::new(
153 &self.context.settings.session.root,
154 &self.context.workspace_root,
155 Some(&self.context.parent_session_id),
156 None,
157 ) {
158 Ok(store) => store,
159 Err(err) => return ToolResult::error(format!("failed to open parent session: {err}")),
160 };
161
162 let task_description = parsed.description.clone();
163
164 let accepted = match self
165 .context
166 .manager
167 .start_or_resume(
168 SubagentRequest {
169 name: parsed.name.clone(),
170 description: parsed.description,
171 prompt: parsed.prompt.clone(),
172 subagent_type: parsed.subagent_type.clone(),
173 resume_task_id: parsed.task_id,
174 parent_session_id: self.context.parent_session_id.clone(),
175 parent_task_id: self.context.parent_task_id.clone(),
176 depth: self.context.depth,
177 },
178 parent_session,
179 )
180 .await
181 {
182 Ok(accepted) => accepted,
183 Err(err) => return ToolResult::error(err.to_string()),
184 };
185
186 let task_id = accepted.task_id;
187 let message = accepted.message;
188
189 let completed = match self
190 .context
191 .manager
192 .wait_for_terminal(&self.context.parent_session_id, &task_id)
193 .await
194 {
195 Ok(node) => node,
196 Err(err) => return ToolResult::error(err.to_string()),
197 };
198
199 let output = TaskToolOutput {
200 task_id,
201 name: completed.name,
202 description: task_description,
203 status: completed.status.label().to_string(),
204 message,
205 agent_name: completed.agent_name,
206 prompt: completed.prompt,
207 depth: completed.depth,
208 parent_task_id: completed.parent_task_id,
209 started_at: completed.started_at,
210 finished_at: Some(completed.updated_at),
211 summary: completed.summary,
212 error: completed.error,
213 };
214
215 ToolResult::ok_json_typed_serializable(
216 "sub-agent completed",
217 "application/vnd.hh.subagent.task+json",
218 &output,
219 )
220 }
221}
222
223fn load_agent_registry() -> Result<AgentRegistry, String> {
224 let loader =
225 AgentLoader::new().map_err(|err| format!("failed to load agent registry: {err}"))?;
226 let agents = loader
227 .load_agents()
228 .map_err(|err| format!("failed to load agents: {err}"))?;
229 Ok(AgentRegistry::new(agents))
230}
231
232fn validate_non_empty(field: &str, value: &str) -> Result<(), ToolResult> {
233 if value.trim().is_empty() {
234 return Err(ToolResult::error(format!("{field} must not be empty")));
235 }
236 Ok(())
237}
238
239fn discover_available_subagents() -> Vec<AvailableSubagent> {
240 let registry = match load_agent_registry() {
241 Ok(registry) => registry,
242 Err(_) => return Vec::new(),
243 };
244
245 let mut subagents = registry
246 .list_agents()
247 .into_iter()
248 .filter(|agent| agent.mode == AgentMode::Subagent)
249 .map(|agent| AvailableSubagent {
250 name: agent.name.clone(),
251 description: agent.description.clone(),
252 })
253 .collect::<Vec<_>>();
254 subagents.sort_by(|left, right| left.name.cmp(&right.name));
255 subagents
256}
257
258fn format_available_subagents(subagents: &[AvailableSubagent]) -> String {
259 if subagents.is_empty() {
260 return "<available_subagents>none</available_subagents>".to_string();
261 }
262
263 let mut description = String::from("<available_subagents>");
264 for subagent in subagents {
265 description.push_str("\n<subagent>");
266 description.push_str("\n<name>");
267 description.push_str(&subagent.name);
268 description.push_str("</name>");
269 description.push_str("\n<description>");
270 description.push_str(&subagent.description);
271 description.push_str("</description>");
272 description.push_str("\n</subagent>");
273 }
274 description.push_str("\n</available_subagents>");
275 description
276}