1use anyhow::Result;
7use std::collections::HashMap;
8use std::path::PathBuf;
9use std::sync::Arc;
10use tokio::sync::mpsc;
11use uuid::Uuid;
12
13use crate::agent::{Agent, AgentBuilder};
14use crate::cancel::CancellationToken;
15use crate::event::AgentEvent;
16use crate::providers::create_minimal_provider;
17use crate::prompt::PromptProfile;
18use crate::tools::Tool;
19
20#[derive(Debug, Clone)]
22pub struct SubagentTask {
23 pub id: String,
25 pub description: String,
27 pub prompt: String,
29 pub subagent_type: String,
31 pub isolation: String,
33 pub work_path: Option<PathBuf>,
35}
36
37#[derive(Debug, Clone)]
39pub struct SubagentResult {
40 pub task_id: String,
42 pub content: String,
44 pub success: bool,
46 pub usage: TokenUsage,
48}
49
50#[derive(Debug, Clone, Default)]
52pub struct TokenUsage {
53 pub input_tokens: u64,
54 pub output_tokens: u64,
55}
56
57#[derive(Debug, Clone)]
59pub struct SubagentConfig {
60 pub model_name: String,
62 pub max_tokens: u32,
64 pub system_prompt_prefix: Option<String>,
66 pub think: bool,
68 pub tool_names: Option<Vec<String>>,
70}
71
72impl Default for SubagentConfig {
73 fn default() -> Self {
74 Self {
75 model_name: "claude-sonnet-4-20250514".to_string(),
76 max_tokens: 4096,
77 system_prompt_prefix: None,
78 think: false,
79 tool_names: None,
80 }
81 }
82}
83
84pub struct SubagentExecutor {
86 config: SubagentConfig,
88 event_tx: mpsc::Sender<AgentEvent>,
90 cancel_tokens: HashMap<String, CancellationToken>,
92 tools: Vec<Arc<dyn Tool>>,
94}
95
96impl SubagentExecutor {
97 pub fn new(
104 config: SubagentConfig,
105 event_tx: mpsc::Sender<AgentEvent>,
106 tools: Vec<Arc<dyn Tool>>,
107 ) -> Self {
108 Self {
109 config,
110 event_tx,
111 cancel_tokens: HashMap::new(),
112 tools,
113 }
114 }
115
116 pub fn with_defaults(
118 event_tx: mpsc::Sender<AgentEvent>,
119 tools: Vec<Arc<dyn Tool>>,
120 ) -> Self {
121 Self::new(SubagentConfig::default(), event_tx, tools)
122 }
123
124 pub async fn execute(&mut self, task: SubagentTask) -> Result<SubagentResult> {
129 let cancel_token = CancellationToken::new();
130 self.cancel_tokens.insert(task.id.clone(), cancel_token.clone());
131
132 let (subagent_tx, mut subagent_rx) = mpsc::channel::<AgentEvent>(100);
134
135 let provider = create_minimal_provider(&self.config.model_name);
137 let work_path = task.work_path.clone().unwrap_or_else(|| {
138 std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
139 });
140
141 let filtered_tools = self.filter_tools(&task.subagent_type);
143
144 let system_prompt = self.build_system_prompt(&task.subagent_type, &task.description);
146
147 let mut agent = AgentBuilder::new(provider)
149 .model_name(&self.config.model_name)
150 .max_tokens(self.config.max_tokens)
151 .system_prompt(system_prompt)
152 .think(self.config.think)
153 .tools_with_provider(filtered_tools)
154 .project_path(work_path)
155 .event_tx(subagent_tx)
156 .profile(PromptProfile::Default)
157 .build();
158
159 agent.set_cancel_token(cancel_token);
160
161 let event_forwarder = tokio::spawn({
163 let main_tx = self.event_tx.clone();
164 let task_id = task.id.clone();
165 async move {
166 while let Some(event) = subagent_rx.recv().await {
167 let tagged_event = Self::tag_event(event, &task_id);
169 if main_tx.send(tagged_event).await.is_err() {
170 break;
171 }
172 }
173 }
174 });
175
176 let run_result = agent.run(task.prompt.clone()).await;
178
179 event_forwarder.abort();
181
182 self.cancel_tokens.remove(&task.id);
184
185 let (input_tokens, output_tokens) = agent.get_token_counts();
187
188 match run_result {
189 Ok(_) => Ok(SubagentResult {
190 task_id: task.id.clone(),
191 content: Self::extract_content(&agent),
192 success: true,
193 usage: TokenUsage {
194 input_tokens,
195 output_tokens,
196 },
197 }),
198 Err(e) => Ok(SubagentResult {
199 task_id: task.id.clone(),
200 content: format!("Task failed: {}", e),
201 success: false,
202 usage: TokenUsage {
203 input_tokens,
204 output_tokens,
205 },
206 }),
207 }
208 }
209
210 pub async fn execute_parallel(&mut self, tasks: Vec<SubagentTask>) -> Result<Vec<SubagentResult>> {
215 let mut results = Vec::with_capacity(tasks.len());
216 let mut futures = Vec::new();
217
218 for task in tasks {
219 let config = self.config.clone();
221 let event_tx = self.event_tx.clone();
222 let tools = self.tools.clone();
223
224 let future = tokio::spawn(async move {
225 let mut executor = SubagentExecutor::new(config, event_tx, tools);
226 executor.execute(task).await
227 });
228
229 futures.push(future);
230 }
231
232 for future in futures {
234 match future.await {
235 Ok(result) => {
236 if let Ok(r) = result {
237 results.push(r);
238 }
239 }
240 Err(e) => {
241 log::error!("Parallel task failed: {}", e);
242 }
243 }
244 }
245
246 Ok(results)
247 }
248
249 pub async fn cancel(&mut self, task_id: &str) -> Result<()> {
251 if let Some(token) = self.cancel_tokens.get(task_id) {
252 token.cancel();
253 log::info!("Task {} cancelled", task_id);
254 }
255 Ok(())
256 }
257
258 pub fn is_cancelled(&self, task_id: &str) -> bool {
260 self.cancel_tokens
261 .get(task_id)
262 .map(|t| t.is_cancelled())
263 .unwrap_or(false)
264 }
265
266 fn filter_tools(&self, subagent_type: &str) -> Vec<Box<dyn Tool>> {
272 let read_only_tools = vec![
274 "read", "grep", "glob", "ls", "search", "code_search",
275 "code_files", "code_node", "code_explore", "code_context",
276 "codegraph_search", "codegraph_files",
277 ];
278
279 let plan_tools = vec![
281 "read", "grep", "glob", "ls", "search", "code_search",
282 "code_files", "code_node", "code_explore", "code_context",
283 "enter_plan_mode", "exit_plan_mode", "todo_write",
284 ];
285
286 let allowed_tools = match subagent_type {
287 "Explore" => &read_only_tools,
288 "Plan" => &plan_tools,
289 _ => return Vec::new(), };
291
292 self.tools
294 .iter()
295 .filter(|t| {
296 let name = t.definition().name;
297 allowed_tools.contains(&name.as_str())
298 })
299 .map(|t| {
300 Self::arc_to_box_tool(t.clone())
303 })
304 .collect()
305 }
306
307 fn arc_to_box_tool(arc_tool: Arc<dyn Tool>) -> Box<dyn Tool> {
310 Box::new(SubagentToolWrapper(arc_tool))
316 }
317
318 fn build_system_prompt(&self, subagent_type: &str, description: &str) -> String {
320 let base_prompt = self.config.system_prompt_prefix.clone()
321 .unwrap_or_else(|| "You are a helpful AI assistant.".to_string());
322
323 match subagent_type {
324 "Explore" => {
325 format!(
326 "{}\n\nYou are an Explore agent focused on fast, read-only search operations.\n\
327 Your task: {}\n\n\
328 Rules:\n\
329 - Only use read-only tools (read, grep, glob, ls, search, code_search)\n\
330 - Do not modify any files\n\
331 - Provide concise summaries of findings\n\
332 - Complete quickly and report results",
333 base_prompt, description
334 )
335 }
336 "Plan" => {
337 format!(
338 "{}\n\nYou are a Plan agent focused on architecture and planning.\n\
339 Your task: {}\n\n\
340 Rules:\n\
341 - Analyze the codebase structure\n\
342 - Create implementation plans\n\
343 - Use todo_write to track plan steps\n\
344 - Provide clear, actionable recommendations\n\
345 - Do not modify files unless explicitly asked",
346 base_prompt, description
347 )
348 }
349 _ => {
350 format!(
351 "{}\n\nYou are a general-purpose agent handling a subtask.\n\
352 Your task: {}\n\n\
353 Complete the task efficiently and report your results.",
354 base_prompt, description
355 )
356 }
357 }
358 }
359
360 fn tag_event(event: AgentEvent, task_id: &str) -> AgentEvent {
362 if let Some(ref data) = event.data {
364 if let crate::event::EventData::Text { delta } = data {
365 return AgentEvent::with_data(
366 event.event_type,
367 crate::event::EventData::Text {
368 delta: format!("[Subagent {}] {}", task_id, delta),
369 },
370 );
371 }
372 }
373 event
374 }
375
376 fn extract_content(agent: &Agent) -> String {
378 let messages = agent.get_messages();
380 for msg in messages.iter().rev() {
381 if msg.role == crate::providers::Role::Assistant {
382 match &msg.content {
383 crate::providers::MessageContent::Text(text) => {
384 return text.clone();
385 }
386 crate::providers::MessageContent::Blocks(blocks) => {
387 let texts: Vec<String> = blocks
389 .iter()
390 .filter_map(|b| {
391 if let crate::providers::ContentBlock::Text { text } = b {
392 Some(text.clone())
393 } else {
394 None
395 }
396 })
397 .collect();
398 return texts.join("\n");
399 }
400 }
401 }
402 }
403 "No output generated".to_string()
404 }
405}
406
407struct SubagentToolWrapper(Arc<dyn Tool>);
410
411#[async_trait::async_trait]
412impl Tool for SubagentToolWrapper {
413 fn definition(&self) -> crate::tools::ToolDefinition {
414 self.0.definition()
415 }
416
417 async fn execute(&self, params: serde_json::Value) -> Result<String> {
418 self.0.execute(params).await
419 }
420
421 fn risk_level(&self) -> crate::approval::RiskLevel {
422 self.0.risk_level()
423 }
424}
425
426pub fn create_task(
428 description: &str,
429 prompt: &str,
430 subagent_type: &str,
431 isolation: &str,
432) -> SubagentTask {
433 let id = Uuid::new_v4().to_string();
434
435 let work_path = if isolation == "worktree" {
436 let temp_dir = std::env::temp_dir()
438 .join(format!("matrixcode-task-{}", id));
439 Some(temp_dir)
440 } else {
441 None
442 };
443
444 SubagentTask {
445 id,
446 description: description.to_string(),
447 prompt: prompt.to_string(),
448 subagent_type: subagent_type.to_string(),
449 isolation: isolation.to_string(),
450 work_path,
451 }
452}
453
454pub async fn setup_worktree(task: &SubagentTask) -> Result<PathBuf> {
456 if task.isolation != "worktree" {
457 return Ok(std::env::current_dir()?);
458 }
459
460 let work_path = task.work_path.clone().unwrap_or_else(|| {
461 std::env::temp_dir()
462 .join(format!("matrixcode-task-{}", task.id))
463 });
464
465 std::fs::create_dir_all(&work_path)?;
467
468 log::info!("Worktree created at {} for task {}", work_path.display(), task.id);
474
475 Ok(work_path)
476}
477
478pub async fn cleanup_worktree(task: &SubagentTask) -> Result<()> {
480 if task.isolation != "worktree" {
481 return Ok(());
482 }
483
484 if let Some(path) = &task.work_path {
485 if path.exists() {
490 std::fs::remove_dir_all(path)?;
491 log::info!("Worktree cleaned up for task {}", task.id);
492 }
493 }
494
495 Ok(())
496}
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501
502 #[test]
503 fn test_create_task() {
504 let task = create_task(
505 "Search codebase",
506 "Find all occurrences of 'Agent'",
507 "Explore",
508 "none",
509 );
510
511 assert_eq!(task.description, "Search codebase");
512 assert_eq!(task.subagent_type, "Explore");
513 assert_eq!(task.isolation, "none");
514 assert!(task.work_path.is_none());
515 }
516
517 #[test]
518 fn test_create_worktree_task() {
519 let task = create_task(
520 "Refactor module",
521 "Refactor the agent module",
522 "general-purpose",
523 "worktree",
524 );
525
526 assert_eq!(task.isolation, "worktree");
527 assert!(task.work_path.is_some());
528 }
529
530 #[test]
531 fn test_subagent_config_default() {
532 let config = SubagentConfig::default();
533
534 assert_eq!(config.model_name, "claude-sonnet-4-20250514");
535 assert_eq!(config.max_tokens, 4096);
536 assert!(!config.think);
537 }
538
539 #[test]
540 fn test_build_system_prompt_explore() {
541 let executor = SubagentExecutor::new(
542 SubagentConfig::default(),
543 tokio::sync::mpsc::channel(1).0,
544 Vec::new(),
545 );
546
547 let prompt = executor.build_system_prompt("Explore", "Search for X");
548
549 assert!(prompt.contains("Explore agent"));
550 assert!(prompt.contains("read-only"));
551 assert!(prompt.contains("Search for X"));
552 }
553
554 #[test]
555 fn test_build_system_prompt_plan() {
556 let executor = SubagentExecutor::new(
557 SubagentConfig::default(),
558 tokio::sync::mpsc::channel(1).0,
559 Vec::new(),
560 );
561
562 let prompt = executor.build_system_prompt("Plan", "Create architecture");
563
564 assert!(prompt.contains("Plan agent"));
565 assert!(prompt.contains("architecture"));
566 assert!(prompt.contains("Create architecture"));
567 }
568}