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",
275 "code_search", "code_callers", "code_callees", "code_status",
276 ];
277
278 let plan_tools = vec![
280 "read", "grep", "glob", "ls", "search",
281 "code_search", "code_callers", "code_callees", "code_status",
282 "enter_plan_mode", "exit_plan_mode", "todo_write",
283 ];
284
285 let allowed_tools = match subagent_type {
286 "Explore" => &read_only_tools,
287 "Plan" => &plan_tools,
288 _ => return Vec::new(), };
290
291 self.tools
293 .iter()
294 .filter(|t| {
295 let name = t.definition().name;
296 allowed_tools.contains(&name.as_str())
297 })
298 .map(|t| {
299 Self::arc_to_box_tool(t.clone())
302 })
303 .collect()
304 }
305
306 fn arc_to_box_tool(arc_tool: Arc<dyn Tool>) -> Box<dyn Tool> {
309 Box::new(SubagentToolWrapper(arc_tool))
315 }
316
317 fn build_system_prompt(&self, subagent_type: &str, description: &str) -> String {
319 let base_prompt = self.config.system_prompt_prefix.clone()
320 .unwrap_or_else(|| "You are a helpful AI assistant.".to_string());
321
322 match subagent_type {
323 "Explore" => {
324 format!(
325 "{}\n\nYou are an Explore agent focused on fast, read-only search operations.\n\
326 Your task: {}\n\n\
327 Rules:\n\
328 - Only use read-only tools (read, grep, glob, ls, search, code_search)\n\
329 - Do not modify any files\n\
330 - Provide concise summaries of findings\n\
331 - Complete quickly and report results",
332 base_prompt, description
333 )
334 }
335 "Plan" => {
336 format!(
337 "{}\n\nYou are a Plan agent focused on architecture and planning.\n\
338 Your task: {}\n\n\
339 Rules:\n\
340 - Analyze the codebase structure\n\
341 - Create implementation plans\n\
342 - Use todo_write to track plan steps\n\
343 - Provide clear, actionable recommendations\n\
344 - Do not modify files unless explicitly asked",
345 base_prompt, description
346 )
347 }
348 _ => {
349 format!(
350 "{}\n\nYou are a general-purpose agent handling a subtask.\n\
351 Your task: {}\n\n\
352 Complete the task efficiently and report your results.",
353 base_prompt, description
354 )
355 }
356 }
357 }
358
359 fn tag_event(event: AgentEvent, task_id: &str) -> AgentEvent {
361 if let Some(ref data) = event.data {
363 if let crate::event::EventData::Text { delta } = data {
364 return AgentEvent::with_data(
365 event.event_type,
366 crate::event::EventData::Text {
367 delta: format!("[Subagent {}] {}", task_id, delta),
368 },
369 );
370 }
371 }
372 event
373 }
374
375 fn extract_content(agent: &Agent) -> String {
377 let messages = agent.get_messages();
379 for msg in messages.iter().rev() {
380 if msg.role == crate::providers::Role::Assistant {
381 match &msg.content {
382 crate::providers::MessageContent::Text(text) => {
383 return text.clone();
384 }
385 crate::providers::MessageContent::Blocks(blocks) => {
386 let texts: Vec<String> = blocks
388 .iter()
389 .filter_map(|b| {
390 if let crate::providers::ContentBlock::Text { text } = b {
391 Some(text.clone())
392 } else {
393 None
394 }
395 })
396 .collect();
397 return texts.join("\n");
398 }
399 }
400 }
401 }
402 "No output generated".to_string()
403 }
404}
405
406struct SubagentToolWrapper(Arc<dyn Tool>);
409
410#[async_trait::async_trait]
411impl Tool for SubagentToolWrapper {
412 fn definition(&self) -> crate::tools::ToolDefinition {
413 self.0.definition()
414 }
415
416 async fn execute(&self, params: serde_json::Value) -> Result<String> {
417 self.0.execute(params).await
418 }
419
420 fn risk_level(&self) -> crate::approval::RiskLevel {
421 self.0.risk_level()
422 }
423}
424
425pub fn create_task(
427 description: &str,
428 prompt: &str,
429 subagent_type: &str,
430 isolation: &str,
431) -> SubagentTask {
432 let id = Uuid::new_v4().to_string();
433
434 let work_path = if isolation == "worktree" {
435 let temp_dir = std::env::temp_dir()
437 .join(format!("matrixcode-task-{}", id));
438 Some(temp_dir)
439 } else {
440 None
441 };
442
443 SubagentTask {
444 id,
445 description: description.to_string(),
446 prompt: prompt.to_string(),
447 subagent_type: subagent_type.to_string(),
448 isolation: isolation.to_string(),
449 work_path,
450 }
451}
452
453pub async fn setup_worktree(task: &SubagentTask) -> Result<PathBuf> {
455 if task.isolation != "worktree" {
456 return Ok(std::env::current_dir()?);
457 }
458
459 let work_path = task.work_path.clone().unwrap_or_else(|| {
460 std::env::temp_dir()
461 .join(format!("matrixcode-task-{}", task.id))
462 });
463
464 std::fs::create_dir_all(&work_path)?;
466
467 log::info!("Worktree created at {} for task {}", work_path.display(), task.id);
473
474 Ok(work_path)
475}
476
477pub async fn cleanup_worktree(task: &SubagentTask) -> Result<()> {
479 if task.isolation != "worktree" {
480 return Ok(());
481 }
482
483 if let Some(path) = &task.work_path {
484 if path.exists() {
489 std::fs::remove_dir_all(path)?;
490 log::info!("Worktree cleaned up for task {}", task.id);
491 }
492 }
493
494 Ok(())
495}
496
497#[cfg(test)]
498mod tests {
499 use super::*;
500
501 #[test]
502 fn test_create_task() {
503 let task = create_task(
504 "Search codebase",
505 "Find all occurrences of 'Agent'",
506 "Explore",
507 "none",
508 );
509
510 assert_eq!(task.description, "Search codebase");
511 assert_eq!(task.subagent_type, "Explore");
512 assert_eq!(task.isolation, "none");
513 assert!(task.work_path.is_none());
514 }
515
516 #[test]
517 fn test_create_worktree_task() {
518 let task = create_task(
519 "Refactor module",
520 "Refactor the agent module",
521 "general-purpose",
522 "worktree",
523 );
524
525 assert_eq!(task.isolation, "worktree");
526 assert!(task.work_path.is_some());
527 }
528
529 #[test]
530 fn test_subagent_config_default() {
531 let config = SubagentConfig::default();
532
533 assert_eq!(config.model_name, "claude-sonnet-4-20250514");
534 assert_eq!(config.max_tokens, 4096);
535 assert!(!config.think);
536 }
537
538 #[test]
539 fn test_build_system_prompt_explore() {
540 let executor = SubagentExecutor::new(
541 SubagentConfig::default(),
542 tokio::sync::mpsc::channel(1).0,
543 Vec::new(),
544 );
545
546 let prompt = executor.build_system_prompt("Explore", "Search for X");
547
548 assert!(prompt.contains("Explore agent"));
549 assert!(prompt.contains("read-only"));
550 assert!(prompt.contains("Search for X"));
551 }
552
553 #[test]
554 fn test_build_system_prompt_plan() {
555 let executor = SubagentExecutor::new(
556 SubagentConfig::default(),
557 tokio::sync::mpsc::channel(1).0,
558 Vec::new(),
559 );
560
561 let prompt = executor.build_system_prompt("Plan", "Create architecture");
562
563 assert!(prompt.contains("Plan agent"));
564 assert!(prompt.contains("architecture"));
565 assert!(prompt.contains("Create architecture"));
566 }
567}