1use crate::code_tools::ast_agent_tools::ASTAgentTools;
13use crate::code_tools::ast_agent_tools::AgentToolOp;
14use crate::code_tools::ast_agent_tools::AgentToolResult;
15use crate::code_tools::ast_agent_tools::DocumentationTarget;
16use crate::code_tools::ast_agent_tools::ImprovementFocus;
17use crate::code_tools::ast_agent_tools::Location;
18use crate::code_tools::ast_agent_tools::PatternType;
19use crate::code_tools::search::SearchScope;
20use crate::subagents::SubagentContext;
21use crate::subagents::SubagentError;
22use crate::subagents::SubagentResult;
23use serde::Deserialize;
24use serde::Serialize;
25use std::collections::HashMap;
26use std::future::Future;
27use std::path::PathBuf;
28use std::pin::Pin;
29use std::sync::Arc;
30use std::sync::atomic::AtomicBool;
31use std::sync::atomic::Ordering;
32use std::time::Duration;
33use std::time::SystemTime;
34use walkdir::WalkDir;
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct AgentResult {
39 pub agent_name: String,
41 pub status: AgentStatus,
43 pub findings: Vec<Finding>,
45 pub analyzed_files: Vec<PathBuf>,
47 pub modified_files: Vec<PathBuf>,
49 pub execution_time: Duration,
51 pub summary: String,
53 pub metrics: HashMap<String, serde_json::Value>,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub enum AgentStatus {
60 Initializing,
61 Analyzing,
62 Processing,
63 Generating,
64 Completed,
65 Failed(String),
66 Cancelled,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct Finding {
72 pub category: String,
74 pub severity: Severity,
76 pub title: String,
78 pub description: String,
80 pub location: Option<Location>,
82 pub suggestion: Option<String>,
84 pub metadata: HashMap<String, serde_json::Value>,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
90pub enum Severity {
91 Info,
92 Low,
93 Medium,
94 High,
95 Critical,
96}
97
98pub trait Subagent: Send + Sync + std::fmt::Debug {
100 fn name(&self) -> &str;
102
103 fn description(&self) -> &str;
105
106 fn execute<'a>(
109 &'a self,
110 context: &'a SubagentContext,
111 ast_tools: &'a mut ASTAgentTools,
112 cancel_flag: Arc<AtomicBool>,
113 ) -> Pin<Box<dyn Future<Output = SubagentResult<AgentResult>> + Send + 'a>>;
114
115 fn capabilities(&self) -> Vec<String>;
117
118 fn supports_file_type(&self, file_path: &std::path::Path) -> bool;
120
121 fn execution_time_estimate(&self) -> Duration {
123 Duration::from_secs(60) }
125}
126
127#[derive(Debug)]
129pub struct CodeReviewerAgent {
130 config: ReviewConfig,
131}
132
133#[derive(Debug, Clone)]
134pub struct ReviewConfig {
135 pub focus_areas: Vec<ReviewFocus>,
136 pub severity_threshold: Severity,
137 pub include_suggestions: bool,
138 pub check_security: bool,
139 pub check_performance: bool,
140 pub check_maintainability: bool,
141}
142
143#[derive(Debug, Clone)]
144pub enum ReviewFocus {
145 Security,
146 Performance,
147 Maintainability,
148 CodeStyle,
149 Documentation,
150 Testing,
151}
152
153impl Default for CodeReviewerAgent {
154 fn default() -> Self {
155 Self::new()
156 }
157}
158
159impl CodeReviewerAgent {
160 pub fn new() -> Self {
161 Self {
162 config: ReviewConfig {
163 focus_areas: vec![
164 ReviewFocus::Security,
165 ReviewFocus::Performance,
166 ReviewFocus::Maintainability,
167 ],
168 severity_threshold: Severity::Low,
169 include_suggestions: true,
170 check_security: true,
171 check_performance: true,
172 check_maintainability: true,
173 },
174 }
175 }
176
177 pub const fn with_config(config: ReviewConfig) -> Self {
178 Self { config }
179 }
180
181 async fn analyze_functions(
182 &self,
183 ast_tools: &mut ASTAgentTools,
184 file: &PathBuf,
185 ) -> SubagentResult<Vec<Finding>> {
186 let mut findings = Vec::new();
187
188 let result = ast_tools.execute(AgentToolOp::ExtractFunctions {
190 file: file.clone(),
191 language: self.detect_language(file),
192 })?;
193
194 if let AgentToolResult::Functions(functions) = result {
195 for func in functions {
196 if let Ok(AgentToolResult::Complexity(complexity)) =
198 ast_tools.execute(AgentToolOp::CalculateComplexity {
199 function: func.name.clone(),
200 })
201 && complexity.cyclomatic_complexity > 10
202 {
203 findings.push(Finding {
204 category: "complexity".to_string(),
205 severity: if complexity.cyclomatic_complexity > 20 {
206 Severity::High
207 } else {
208 Severity::Medium
209 },
210 title: format!("High cyclomatic complexity in {}", func.name),
211 description: format!(
212 "Function '{}' has cyclomatic complexity of {}, consider refactoring",
213 func.name, complexity.cyclomatic_complexity
214 ),
215 location: Some(Location {
216 file: file.clone(),
217 line: func.start_line,
218 column: 0,
219 byte_offset: 0,
220 }),
221 suggestion: Some(
222 "Break down into smaller functions or reduce conditional complexity"
223 .to_string(),
224 ),
225 metadata: HashMap::from([(
226 "complexity".to_string(),
227 serde_json::Value::Number(serde_json::Number::from(
228 complexity.cyclomatic_complexity,
229 )),
230 )]),
231 });
232 }
233
234 let line_count = func.end_line - func.start_line + 1;
236 if line_count > 50 {
237 findings.push(Finding {
238 category: "maintainability".to_string(),
239 severity: if line_count > 100 {
240 Severity::High
241 } else {
242 Severity::Medium
243 },
244 title: format!("Long function: {}", func.name),
245 description: format!(
246 "Function '{}' is {} lines long, consider breaking it down",
247 func.name, line_count
248 ),
249 location: Some(Location {
250 file: file.clone(),
251 line: func.start_line,
252 column: 0,
253 byte_offset: 0,
254 }),
255 suggestion: Some("Extract smaller, focused functions".to_string()),
256 metadata: HashMap::from([(
257 "line_count".to_string(),
258 serde_json::Value::Number(serde_json::Number::from(line_count)),
259 )]),
260 });
261 }
262
263 if func.parameters.len() > 5 {
265 findings.push(Finding {
266 category: "design".to_string(),
267 severity: Severity::Medium,
268 title: format!("Too many parameters in {}", func.name),
269 description: format!(
270 "Function '{}' has {} parameters, consider using a parameter object",
271 func.name,
272 func.parameters.len()
273 ),
274 location: Some(Location {
275 file: file.clone(),
276 line: func.start_line,
277 column: 0,
278 byte_offset: 0,
279 }),
280 suggestion: Some(
281 "Group related parameters into a struct or use builder pattern"
282 .to_string(),
283 ),
284 metadata: HashMap::from([(
285 "parameter_count".to_string(),
286 serde_json::Value::Number(serde_json::Number::from(
287 func.parameters.len(),
288 )),
289 )]),
290 });
291 }
292 }
293 }
294
295 Ok(findings)
296 }
297
298 async fn detect_security_issues(
299 &self,
300 ast_tools: &mut ASTAgentTools,
301 ) -> SubagentResult<Vec<Finding>> {
302 let mut findings = Vec::new();
303
304 let result = ast_tools.execute(AgentToolOp::DetectPatterns {
306 pattern: PatternType::AntiPattern("sql_injection".to_string()),
307 })?;
308
309 if let AgentToolResult::Patterns(patterns) = result {
310 for pattern in patterns {
311 findings.push(Finding {
312 category: "security".to_string(),
313 severity: Severity::Critical,
314 title: "Potential SQL injection vulnerability".to_string(),
315 description: "Direct string concatenation for SQL queries detected".to_string(),
316 location: Some(pattern.location),
317 suggestion: Some(
318 "Use parameterized queries or prepared statements".to_string(),
319 ),
320 metadata: HashMap::new(),
321 });
322 }
323 }
324
325 Ok(findings)
326 }
327
328 async fn find_dead_code(&self, ast_tools: &mut ASTAgentTools) -> SubagentResult<Vec<Finding>> {
329 let mut findings = Vec::new();
330
331 let result = ast_tools.execute(AgentToolOp::FindDeadCode {
332 scope: SearchScope::Workspace,
333 })?;
334
335 if let AgentToolResult::DeadCode(dead_items) = result {
336 for item in dead_items {
337 findings.push(Finding {
338 category: "maintainability".to_string(),
339 severity: Severity::Low,
340 title: format!("Dead code detected: {}", item.symbol),
341 description: format!("Unused {:?} '{}' found", item.kind, item.symbol),
342 location: Some(item.location),
343 suggestion: Some("Remove unused code to improve maintainability".to_string()),
344 metadata: HashMap::from([(
345 "item_type".to_string(),
346 serde_json::Value::String(format!("{:?}", item.kind)),
347 )]),
348 });
349 }
350 }
351
352 Ok(findings)
353 }
354
355 fn detect_language(&self, file: &PathBuf) -> String {
356 match file.extension().and_then(|ext| ext.to_str()) {
357 Some("rs") => "rust".to_string(),
358 Some("py") => "python".to_string(),
359 Some("js") | Some("jsx") => "javascript".to_string(),
360 Some("ts") | Some("tsx") => "typescript".to_string(),
361 Some("go") => "go".to_string(),
362 Some("java") => "java".to_string(),
363 Some("cpp") | Some("cc") | Some("cxx") => "cpp".to_string(),
364 Some("c") => "c".to_string(),
365 Some("cs") => "csharp".to_string(),
366 _ => "text".to_string(),
367 }
368 }
369}
370
371impl Subagent for CodeReviewerAgent {
372 fn name(&self) -> &str {
373 "code-reviewer"
374 }
375
376 fn description(&self) -> &str {
377 "AST-based code review for quality, security, and maintainability analysis"
378 }
379
380 fn execute<'a>(
381 &'a self,
382 context: &'a SubagentContext,
383 ast_tools: &'a mut ASTAgentTools,
384 cancel_flag: Arc<AtomicBool>,
385 ) -> Pin<Box<dyn Future<Output = SubagentResult<AgentResult>> + Send + 'a>> {
386 Box::pin(async move {
387 let start_time = SystemTime::now();
388 let mut all_findings = Vec::new();
389 let mut analyzed_files = Vec::new();
390
391 let files_to_analyze = self.get_files_to_analyze(&context.working_directory)?;
393
394 for file in &files_to_analyze {
395 if cancel_flag.load(Ordering::Relaxed) {
396 return Ok(AgentResult {
397 agent_name: self.name().to_string(),
398 status: AgentStatus::Cancelled,
399 findings: all_findings,
400 analyzed_files,
401 modified_files: vec![],
402 execution_time: start_time.elapsed().unwrap_or_default(),
403 summary: "Analysis cancelled by user".to_string(),
404 metrics: HashMap::new(),
405 });
406 }
407
408 analyzed_files.push(file.clone());
409
410 let mut findings = self.analyze_functions(ast_tools, file).await?;
412 all_findings.append(&mut findings);
413 }
414
415 if self.config.check_security {
417 let mut security_findings = self.detect_security_issues(ast_tools).await?;
418 all_findings.append(&mut security_findings);
419 }
420
421 let mut dead_code_findings = self.find_dead_code(ast_tools).await?;
423 all_findings.append(&mut dead_code_findings);
424
425 let summary = self.generate_summary(&all_findings);
427 let metrics = self.calculate_metrics(&all_findings, &analyzed_files);
428
429 Ok(AgentResult {
430 agent_name: self.name().to_string(),
431 status: AgentStatus::Completed,
432 findings: all_findings,
433 analyzed_files,
434 modified_files: vec![],
435 execution_time: start_time.elapsed().unwrap_or_default(),
436 summary,
437 metrics,
438 })
439 })
440 }
441
442 fn capabilities(&self) -> Vec<String> {
443 vec![
444 "extract_functions".to_string(),
445 "detect_patterns".to_string(),
446 "find_dead_code".to_string(),
447 "calculate_complexity".to_string(),
448 ]
449 }
450
451 fn supports_file_type(&self, file_path: &std::path::Path) -> bool {
452 matches!(
453 file_path.extension().and_then(|ext| ext.to_str()),
454 Some("rs")
455 | Some("py")
456 | Some("js")
457 | Some("ts")
458 | Some("jsx")
459 | Some("tsx")
460 | Some("go")
461 | Some("java")
462 | Some("cpp")
463 | Some("c")
464 | Some("cs")
465 )
466 }
467
468 fn execution_time_estimate(&self) -> Duration {
469 Duration::from_secs(120) }
471}
472
473impl CodeReviewerAgent {
474 fn get_files_to_analyze(&self, working_dir: &PathBuf) -> SubagentResult<Vec<PathBuf>> {
475 let mut files = Vec::new();
476
477 for entry in WalkDir::new(working_dir).follow_links(false).max_depth(10) {
479 let entry =
480 entry.map_err(|e| SubagentError::Io(std::io::Error::other(e.to_string())))?;
481
482 if entry.file_type().is_file() && self.supports_file_type(entry.path()) {
483 files.push(entry.path().to_path_buf());
484 }
485 }
486
487 Ok(files)
488 }
489
490 fn generate_summary(&self, findings: &[Finding]) -> String {
491 let critical_count = findings
492 .iter()
493 .filter(|f| f.severity == Severity::Critical)
494 .count();
495 let high_count = findings
496 .iter()
497 .filter(|f| f.severity == Severity::High)
498 .count();
499 let medium_count = findings
500 .iter()
501 .filter(|f| f.severity == Severity::Medium)
502 .count();
503 let low_count = findings
504 .iter()
505 .filter(|f| f.severity == Severity::Low)
506 .count();
507
508 format!(
509 "Code review completed. Found {} issues: {} critical, {} high, {} medium, {} low severity.",
510 findings.len(),
511 critical_count,
512 high_count,
513 medium_count,
514 low_count
515 )
516 }
517
518 fn calculate_metrics(
519 &self,
520 findings: &[Finding],
521 analyzed_files: &[PathBuf],
522 ) -> HashMap<String, serde_json::Value> {
523 let mut metrics = HashMap::new();
524
525 metrics.insert(
526 "total_findings".to_string(),
527 serde_json::Value::Number(serde_json::Number::from(findings.len())),
528 );
529 metrics.insert(
530 "files_analyzed".to_string(),
531 serde_json::Value::Number(serde_json::Number::from(analyzed_files.len())),
532 );
533
534 for severity in [
536 Severity::Critical,
537 Severity::High,
538 Severity::Medium,
539 Severity::Low,
540 Severity::Info,
541 ] {
542 let count = findings.iter().filter(|f| f.severity == severity).count();
543 let key = format!("{:?}_count", severity).to_lowercase();
544 metrics.insert(
545 key,
546 serde_json::Value::Number(serde_json::Number::from(count)),
547 );
548 }
549
550 let mut categories = HashMap::new();
552 for finding in findings {
553 *categories.entry(finding.category.clone()).or_insert(0) += 1;
554 }
555
556 for (category, count) in categories {
557 metrics.insert(
558 format!("{}_issues", category),
559 serde_json::Value::Number(serde_json::Number::from(count)),
560 );
561 }
562
563 metrics
564 }
565}
566
567#[derive(Debug)]
569pub struct RefactorerAgent {
570 _config: RefactorConfig,
571}
572
573#[derive(Debug, Clone)]
574pub struct RefactorConfig {
575 pub risk_threshold: RiskLevel,
576 pub operations: Vec<RefactorOperation>,
577 pub backup_enabled: bool,
578}
579
580#[derive(Debug, Clone, Copy, PartialEq, Eq)]
581pub enum RiskLevel {
582 Low,
583 Medium,
584 High,
585}
586
587#[derive(Debug, Clone)]
588pub enum RefactorOperation {
589 ExtractMethod,
590 InlineVariable,
591 RenameSymbol,
592 RemoveDuplication,
593}
594
595impl Default for RefactorerAgent {
596 fn default() -> Self {
597 Self::new()
598 }
599}
600
601impl RefactorerAgent {
602 pub fn new() -> Self {
603 Self {
604 _config: RefactorConfig {
605 risk_threshold: RiskLevel::Medium,
606 operations: vec![
607 RefactorOperation::ExtractMethod,
608 RefactorOperation::RemoveDuplication,
609 ],
610 backup_enabled: true,
611 },
612 }
613 }
614}
615
616impl Subagent for RefactorerAgent {
617 fn name(&self) -> &str {
618 "refactorer"
619 }
620
621 fn description(&self) -> &str {
622 "Systematic code refactoring with risk assessment and automated improvements"
623 }
624
625 fn execute<'a>(
626 &'a self,
627 _context: &'a SubagentContext,
628 ast_tools: &'a mut ASTAgentTools,
629 _cancel_flag: Arc<AtomicBool>,
630 ) -> Pin<Box<dyn Future<Output = SubagentResult<AgentResult>> + Send + 'a>> {
631 Box::pin(async move {
632 let start_time = SystemTime::now();
633 let mut findings = Vec::new();
634
635 let result = ast_tools.execute(AgentToolOp::DetectDuplication { threshold: 0.8 })?;
637
638 if let AgentToolResult::Duplications(duplications) = result {
639 for dup_group in duplications {
640 findings.push(Finding {
641 category: "duplication".to_string(),
642 severity: if dup_group.locations.len() > 3 {
643 Severity::High
644 } else {
645 Severity::Medium
646 },
647 title: "Code duplication detected".to_string(),
648 description: format!(
649 "Found {} instances of duplicated code",
650 dup_group.locations.len()
651 ),
652 location: dup_group.locations.first().cloned(),
653 suggestion: Some(
654 "Extract common functionality into a shared function or method"
655 .to_string(),
656 ),
657 metadata: HashMap::from([
658 (
659 "duplication_count".to_string(),
660 serde_json::Value::Number(serde_json::Number::from(
661 dup_group.locations.len(),
662 )),
663 ),
664 (
665 "similarity_score".to_string(),
666 serde_json::Value::Number(
667 serde_json::Number::from_f64(dup_group.similarity as f64)
668 .unwrap(),
669 ),
670 ),
671 ]),
672 });
673 }
674 }
675
676 Ok(AgentResult {
677 agent_name: self.name().to_string(),
678 status: AgentStatus::Completed,
679 findings,
680 analyzed_files: vec![],
681 modified_files: vec![],
682 execution_time: start_time.elapsed().unwrap_or_default(),
683 summary: "Refactoring analysis completed".to_string(),
684 metrics: HashMap::new(),
685 })
686 })
687 }
688
689 fn capabilities(&self) -> Vec<String> {
690 vec![
691 "detect_duplication".to_string(),
692 "extract_method".to_string(),
693 "inline_variable".to_string(),
694 "refactor_rename".to_string(),
695 ]
696 }
697
698 fn supports_file_type(&self, file_path: &std::path::Path) -> bool {
699 matches!(
701 file_path.extension().and_then(|ext| ext.to_str()),
702 Some("rs")
703 | Some("py")
704 | Some("js")
705 | Some("ts")
706 | Some("jsx")
707 | Some("tsx")
708 | Some("go")
709 | Some("java")
710 | Some("cpp")
711 | Some("c")
712 | Some("cs")
713 )
714 }
715}
716
717#[derive(Debug)]
719pub struct DebuggerAgent;
720
721impl Default for DebuggerAgent {
722 fn default() -> Self {
723 Self::new()
724 }
725}
726
727impl DebuggerAgent {
728 pub const fn new() -> Self {
729 Self
730 }
731}
732
733impl Subagent for DebuggerAgent {
734 fn name(&self) -> &str {
735 "debugger"
736 }
737
738 fn description(&self) -> &str {
739 "Deep debugging analysis with execution path tracing and error source identification"
740 }
741
742 fn execute<'a>(
743 &'a self,
744 context: &'a SubagentContext,
745 ast_tools: &'a mut ASTAgentTools,
746 _cancel_flag: Arc<AtomicBool>,
747 ) -> Pin<Box<dyn Future<Output = SubagentResult<AgentResult>> + Send + 'a>> {
748 Box::pin(async move {
749 let start_time = SystemTime::now();
750 let mut findings = Vec::new();
751
752 if let Some(entry_point) = context.parameters.get("entry_point") {
754 let result = ast_tools.execute(AgentToolOp::AnalyzeCallGraph {
755 entry_point: entry_point.clone(),
756 })?;
757
758 if let AgentToolResult::CallGraph(call_graph) = result {
759 for (_name, node) in call_graph.nodes {
761 let has_callers = call_graph
763 .edges
764 .iter()
765 .any(|edge| edge.callee == node.function_name);
766 if !has_callers {
767 findings.push(Finding {
768 category: "unreachable".to_string(),
769 severity: Severity::Medium,
770 title: format!(
771 "Potentially unreachable function: {}",
772 node.function_name
773 ),
774 description: "Function with no callers detected".to_string(),
775 location: Some(node.location),
776 suggestion: Some(
777 "Verify if this function is used externally or can be removed"
778 .to_string(),
779 ),
780 metadata: HashMap::new(),
781 });
782 }
783 }
784 }
785 }
786
787 Ok(AgentResult {
788 agent_name: self.name().to_string(),
789 status: AgentStatus::Completed,
790 findings,
791 analyzed_files: vec![],
792 modified_files: vec![],
793 execution_time: start_time.elapsed().unwrap_or_default(),
794 summary: "Debugging analysis completed".to_string(),
795 metrics: HashMap::new(),
796 })
797 })
798 }
799
800 fn capabilities(&self) -> Vec<String> {
801 vec![
802 "analyze_call_graph".to_string(),
803 "find_references".to_string(),
804 "find_definition".to_string(),
805 ]
806 }
807
808 fn supports_file_type(&self, file_path: &std::path::Path) -> bool {
809 matches!(
810 file_path.extension().and_then(|ext| ext.to_str()),
811 Some("rs")
812 | Some("py")
813 | Some("js")
814 | Some("ts")
815 | Some("jsx")
816 | Some("tsx")
817 | Some("go")
818 | Some("java")
819 | Some("cpp")
820 | Some("c")
821 | Some("cs")
822 )
823 }
824}
825
826#[derive(Debug)]
828pub struct TestWriterAgent;
829
830impl Default for TestWriterAgent {
831 fn default() -> Self {
832 Self::new()
833 }
834}
835
836impl TestWriterAgent {
837 pub const fn new() -> Self {
838 Self
839 }
840}
841
842impl Subagent for TestWriterAgent {
843 fn name(&self) -> &str {
844 "test-writer"
845 }
846
847 fn description(&self) -> &str {
848 "Comprehensive test generation and coverage analysis"
849 }
850
851 fn execute<'a>(
852 &'a self,
853 context: &'a SubagentContext,
854 ast_tools: &'a mut ASTAgentTools,
855 _cancel_flag: Arc<AtomicBool>,
856 ) -> Pin<Box<dyn Future<Output = SubagentResult<AgentResult>> + Send + 'a>> {
857 Box::pin(async move {
858 let start_time = SystemTime::now();
859 let mut findings = Vec::new();
860
861 let files = self.get_source_files(&context.working_directory)?;
863
864 for file in &files {
865 let result = ast_tools.execute(AgentToolOp::ExtractFunctions {
866 file: file.clone(),
867 language: self.detect_language(file),
868 })?;
869
870 if let AgentToolResult::Functions(functions) = result {
871 for func in functions {
872 let is_test_function =
874 func.name.contains("test") || func.name.starts_with("test_");
875 if !is_test_function && func.is_exported {
876 findings.push(Finding {
877 category: "testing".to_string(),
878 severity: Severity::Medium,
879 title: format!("Missing tests for function: {}", func.name),
880 description: format!(
881 "Public function '{}' lacks test coverage",
882 func.name
883 ),
884 location: Some(Location {
885 file: file.clone(),
886 line: func.start_line,
887 column: 0,
888 byte_offset: 0,
889 }),
890 suggestion: Some(
891 "Add unit tests to verify function behavior".to_string(),
892 ),
893 metadata: HashMap::from([
894 (
895 "function_name".to_string(),
896 serde_json::Value::String(func.name),
897 ),
898 (
899 "parameter_count".to_string(),
900 serde_json::Value::Number(serde_json::Number::from(
901 func.parameters.len(),
902 )),
903 ),
904 ]),
905 });
906 }
907 }
908 }
909 }
910
911 let findings_count = findings.len();
912 Ok(AgentResult {
913 agent_name: self.name().to_string(),
914 status: AgentStatus::Completed,
915 findings,
916 analyzed_files: files,
917 modified_files: vec![],
918 execution_time: start_time.elapsed().unwrap_or_default(),
919 summary: format!(
920 "Test analysis completed. Found {} functions needing tests.",
921 findings_count
922 ),
923 metrics: HashMap::new(),
924 })
925 })
926 }
927
928 fn capabilities(&self) -> Vec<String> {
929 vec![
930 "extract_functions".to_string(),
931 "generate_tests".to_string(),
932 ]
933 }
934
935 fn supports_file_type(&self, file_path: &std::path::Path) -> bool {
936 matches!(
937 file_path.extension().and_then(|ext| ext.to_str()),
938 Some("rs")
939 | Some("py")
940 | Some("js")
941 | Some("ts")
942 | Some("jsx")
943 | Some("tsx")
944 | Some("go")
945 | Some("java")
946 | Some("cpp")
947 | Some("c")
948 | Some("cs")
949 )
950 }
951}
952
953impl TestWriterAgent {
954 fn get_source_files(&self, working_dir: &PathBuf) -> SubagentResult<Vec<PathBuf>> {
955 let mut files = Vec::new();
956
957 for entry in WalkDir::new(working_dir).follow_links(false).max_depth(8) {
958 let entry =
959 entry.map_err(|e| SubagentError::Io(std::io::Error::other(e.to_string())))?;
960
961 if entry.file_type().is_file() && self.supports_file_type(entry.path()) {
962 let path_str = entry.path().to_string_lossy();
964 if !path_str.contains("test") && !path_str.contains("spec") {
965 files.push(entry.path().to_path_buf());
966 }
967 }
968 }
969
970 Ok(files)
971 }
972
973 fn detect_language(&self, file: &PathBuf) -> String {
974 match file.extension().and_then(|ext| ext.to_str()) {
975 Some("rs") => "rust".to_string(),
976 Some("py") => "python".to_string(),
977 Some("js") | Some("jsx") => "javascript".to_string(),
978 Some("ts") | Some("tsx") => "typescript".to_string(),
979 Some("go") => "go".to_string(),
980 Some("java") => "java".to_string(),
981 _ => "text".to_string(),
982 }
983 }
984}
985
986#[derive(Debug)]
988pub struct PerformanceAgent;
989
990impl Default for PerformanceAgent {
991 fn default() -> Self {
992 Self::new()
993 }
994}
995
996impl PerformanceAgent {
997 pub const fn new() -> Self {
998 Self
999 }
1000}
1001
1002impl Subagent for PerformanceAgent {
1003 fn name(&self) -> &str {
1004 "performance"
1005 }
1006
1007 fn description(&self) -> &str {
1008 "Performance optimization with bottleneck detection and algorithmic analysis"
1009 }
1010
1011 fn execute<'a>(
1012 &'a self,
1013 context: &'a SubagentContext,
1014 ast_tools: &'a mut ASTAgentTools,
1015 _cancel_flag: Arc<AtomicBool>,
1016 ) -> Pin<Box<dyn Future<Output = SubagentResult<AgentResult>> + Send + 'a>> {
1017 Box::pin(async move {
1018 let start_time = SystemTime::now();
1019 let mut findings = Vec::new();
1020
1021 let target_file = context.working_directory.join("src").join("main.rs");
1024 if target_file.exists() {
1025 let result = ast_tools.execute(AgentToolOp::SuggestImprovements {
1026 file: target_file,
1027 focus: ImprovementFocus::Performance,
1028 })?;
1029
1030 if let AgentToolResult::Improvements(improvements) = result {
1031 for improvement in improvements {
1032 let severity = Severity::Medium;
1033
1034 findings.push(Finding {
1035 category: "performance".to_string(),
1036 severity,
1037 title: format!("Performance improvement: {:?}", improvement.category),
1038 description: improvement.description,
1039 location: Some(improvement.location),
1040 suggestion: improvement.suggested_change,
1041 metadata: HashMap::from([
1042 (
1043 "category".to_string(),
1044 serde_json::Value::String(format!(
1045 "{:?}",
1046 improvement.category
1047 )),
1048 ),
1049 (
1050 "impact".to_string(),
1051 serde_json::Value::String(format!("{:?}", improvement.impact)),
1052 ),
1053 ]),
1054 });
1055 }
1056 }
1057 }
1058
1059 let findings_count = findings.len();
1060 Ok(AgentResult {
1061 agent_name: self.name().to_string(),
1062 status: AgentStatus::Completed,
1063 findings,
1064 analyzed_files: vec![],
1065 modified_files: vec![],
1066 execution_time: start_time.elapsed().unwrap_or_default(),
1067 summary: format!(
1068 "Performance analysis completed. Found {} optimization opportunities.",
1069 findings_count
1070 ),
1071 metrics: HashMap::new(),
1072 })
1073 })
1074 }
1075
1076 fn capabilities(&self) -> Vec<String> {
1077 vec![
1078 "suggest_improvements".to_string(),
1079 "calculate_complexity".to_string(),
1080 "analyze_call_graph".to_string(),
1081 ]
1082 }
1083
1084 fn supports_file_type(&self, file_path: &std::path::Path) -> bool {
1085 matches!(
1086 file_path.extension().and_then(|ext| ext.to_str()),
1087 Some("rs")
1088 | Some("py")
1089 | Some("js")
1090 | Some("ts")
1091 | Some("jsx")
1092 | Some("tsx")
1093 | Some("go")
1094 | Some("java")
1095 | Some("cpp")
1096 | Some("c")
1097 | Some("cs")
1098 )
1099 }
1100}
1101
1102#[derive(Debug)]
1104pub struct SecurityAgent;
1105
1106impl Default for SecurityAgent {
1107 fn default() -> Self {
1108 Self::new()
1109 }
1110}
1111
1112impl SecurityAgent {
1113 pub const fn new() -> Self {
1114 Self
1115 }
1116}
1117
1118impl Subagent for SecurityAgent {
1119 fn name(&self) -> &str {
1120 "security"
1121 }
1122
1123 fn description(&self) -> &str {
1124 "Security vulnerability detection with OWASP Top 10 analysis"
1125 }
1126
1127 fn execute<'a>(
1128 &'a self,
1129 _context: &'a SubagentContext,
1130 ast_tools: &'a mut ASTAgentTools,
1131 _cancel_flag: Arc<AtomicBool>,
1132 ) -> Pin<Box<dyn Future<Output = SubagentResult<AgentResult>> + Send + 'a>> {
1133 Box::pin(async move {
1134 let start_time = SystemTime::now();
1135 let mut findings = Vec::new();
1136
1137 let security_patterns = vec![
1139 "sql_injection",
1140 "xss_vulnerability",
1141 "path_traversal",
1142 "hardcoded_secrets",
1143 "weak_crypto",
1144 ];
1145
1146 for pattern in security_patterns {
1147 let result = ast_tools.execute(AgentToolOp::DetectPatterns {
1148 pattern: PatternType::AntiPattern(pattern.to_string()),
1149 })?;
1150
1151 if let AgentToolResult::Patterns(patterns) = result {
1152 for detected_pattern in patterns {
1153 let (severity, description, suggestion) = match pattern {
1154 "sql_injection" => (
1155 Severity::Critical,
1156 "Potential SQL injection vulnerability detected",
1157 "Use parameterized queries or prepared statements",
1158 ),
1159 "xss_vulnerability" => (
1160 Severity::High,
1161 "Potential XSS vulnerability detected",
1162 "Sanitize and validate all user inputs",
1163 ),
1164 "path_traversal" => (
1165 Severity::High,
1166 "Potential path traversal vulnerability",
1167 "Validate and sanitize file paths",
1168 ),
1169 "hardcoded_secrets" => (
1170 Severity::Critical,
1171 "Hardcoded secret or credential detected",
1172 "Use environment variables or secure credential storage",
1173 ),
1174 "weak_crypto" => (
1175 Severity::Medium,
1176 "Weak cryptographic algorithm detected",
1177 "Use modern, secure cryptographic algorithms",
1178 ),
1179 _ => (
1180 Severity::Medium,
1181 "Security issue detected",
1182 "Review and address security concern",
1183 ),
1184 };
1185
1186 findings.push(Finding {
1187 category: "security".to_string(),
1188 severity,
1189 title: format!("Security: {}", pattern.replace('_', " ")),
1190 description: description.to_string(),
1191 location: Some(detected_pattern.location),
1192 suggestion: Some(suggestion.to_string()),
1193 metadata: HashMap::from([(
1194 "vulnerability_type".to_string(),
1195 serde_json::Value::String(pattern.to_string()),
1196 )]),
1197 });
1198 }
1199 }
1200 }
1201
1202 let findings_count = findings.len();
1203 Ok(AgentResult {
1204 agent_name: self.name().to_string(),
1205 status: AgentStatus::Completed,
1206 findings,
1207 analyzed_files: vec![],
1208 modified_files: vec![],
1209 execution_time: start_time.elapsed().unwrap_or_default(),
1210 summary: format!(
1211 "Security analysis completed. Found {} potential vulnerabilities.",
1212 findings_count
1213 ),
1214 metrics: HashMap::new(),
1215 })
1216 })
1217 }
1218
1219 fn capabilities(&self) -> Vec<String> {
1220 vec!["detect_patterns".to_string(), "security_scan".to_string()]
1221 }
1222
1223 fn supports_file_type(&self, file_path: &std::path::Path) -> bool {
1224 matches!(
1225 file_path.extension().and_then(|ext| ext.to_str()),
1226 Some("rs")
1227 | Some("py")
1228 | Some("js")
1229 | Some("ts")
1230 | Some("jsx")
1231 | Some("tsx")
1232 | Some("go")
1233 | Some("java")
1234 | Some("cpp")
1235 | Some("c")
1236 | Some("cs")
1237 | Some("php")
1238 )
1239 }
1240}
1241
1242#[derive(Debug)]
1244pub struct DocsAgent;
1245
1246impl Default for DocsAgent {
1247 fn default() -> Self {
1248 Self::new()
1249 }
1250}
1251
1252impl DocsAgent {
1253 pub const fn new() -> Self {
1254 Self
1255 }
1256}
1257
1258impl Subagent for DocsAgent {
1259 fn name(&self) -> &str {
1260 "docs"
1261 }
1262
1263 fn description(&self) -> &str {
1264 "Documentation generation and API reference creation"
1265 }
1266
1267 fn execute<'a>(
1268 &'a self,
1269 context: &'a SubagentContext,
1270 ast_tools: &'a mut ASTAgentTools,
1271 _cancel_flag: Arc<AtomicBool>,
1272 ) -> Pin<Box<dyn Future<Output = SubagentResult<AgentResult>> + Send + 'a>> {
1273 Box::pin(async move {
1274 let start_time = SystemTime::now();
1275 let mut findings = Vec::new();
1276
1277 if let Some(target_file) = context.parameters.get("target") {
1279 let target_path = PathBuf::from(target_file);
1280
1281 let result = ast_tools.execute(AgentToolOp::GenerateDocumentation {
1282 target: DocumentationTarget::File(target_path),
1283 })?;
1284
1285 if let AgentToolResult::Documentation(doc_content) = result {
1286 findings.push(Finding {
1287 category: "documentation".to_string(),
1288 severity: Severity::Info,
1289 title: "Generated documentation".to_string(),
1290 description: "API documentation has been generated".to_string(),
1291 location: None,
1292 suggestion: None,
1293 metadata: HashMap::from([(
1294 "doc_length".to_string(),
1295 serde_json::Value::Number(serde_json::Number::from(doc_content.len())),
1296 )]),
1297 });
1298 }
1299 }
1300
1301 Ok(AgentResult {
1302 agent_name: self.name().to_string(),
1303 status: AgentStatus::Completed,
1304 findings,
1305 analyzed_files: vec![],
1306 modified_files: vec![],
1307 execution_time: start_time.elapsed().unwrap_or_default(),
1308 summary: "Documentation generation completed".to_string(),
1309 metrics: HashMap::new(),
1310 })
1311 })
1312 }
1313
1314 fn capabilities(&self) -> Vec<String> {
1315 vec![
1316 "generate_documentation".to_string(),
1317 "extract_functions".to_string(),
1318 ]
1319 }
1320
1321 fn supports_file_type(&self, file_path: &std::path::Path) -> bool {
1322 matches!(
1323 file_path.extension().and_then(|ext| ext.to_str()),
1324 Some("rs")
1325 | Some("py")
1326 | Some("js")
1327 | Some("ts")
1328 | Some("jsx")
1329 | Some("tsx")
1330 | Some("go")
1331 | Some("java")
1332 | Some("cpp")
1333 | Some("c")
1334 | Some("cs")
1335 )
1336 }
1337}
1338
1339pub struct AgentRegistry {
1341 agents: HashMap<String, Arc<dyn Subagent>>,
1342}
1343
1344impl AgentRegistry {
1345 pub fn new() -> Self {
1346 let mut registry = Self {
1347 agents: HashMap::new(),
1348 };
1349
1350 registry.register(Arc::new(CodeReviewerAgent::new()));
1352 registry.register(Arc::new(RefactorerAgent::new()));
1353 registry.register(Arc::new(DebuggerAgent::new()));
1354 registry.register(Arc::new(TestWriterAgent::new()));
1355 registry.register(Arc::new(PerformanceAgent::new()));
1356 registry.register(Arc::new(SecurityAgent::new()));
1357 registry.register(Arc::new(DocsAgent::new()));
1358
1359 registry
1360 }
1361
1362 pub fn register(&mut self, agent: Arc<dyn Subagent>) {
1363 self.agents.insert(agent.name().to_string(), agent);
1364 }
1365
1366 pub fn get_agent(&self, name: &str) -> Option<Arc<dyn Subagent>> {
1367 self.agents.get(name).cloned()
1368 }
1369
1370 pub fn list_agents(&self) -> Vec<&str> {
1371 self.agents.keys().map(|s| s.as_str()).collect()
1372 }
1373
1374 pub fn get_agents_for_file(&self, file_path: &std::path::Path) -> Vec<String> {
1375 self.agents
1376 .iter()
1377 .filter(|(_, agent)| agent.supports_file_type(file_path))
1378 .map(|(name, _)| name.clone())
1379 .collect()
1380 }
1381}
1382
1383impl Default for AgentRegistry {
1384 fn default() -> Self {
1385 Self::new()
1386 }
1387}
1388
1389#[cfg(test)]
1390mod tests {
1391 use super::*;
1392
1393 #[test]
1394 fn test_agent_registry() {
1395 let registry = AgentRegistry::new();
1396
1397 assert!(registry.get_agent("code-reviewer").is_some());
1398 assert!(registry.get_agent("refactorer").is_some());
1399 assert!(registry.get_agent("debugger").is_some());
1400 assert!(registry.get_agent("test-writer").is_some());
1401 assert!(registry.get_agent("performance").is_some());
1402 assert!(registry.get_agent("security").is_some());
1403 assert!(registry.get_agent("docs").is_some());
1404
1405 assert_eq!(registry.list_agents().len(), 7);
1406 }
1407
1408 #[test]
1409 fn test_file_type_support() {
1410 let agent = CodeReviewerAgent::new();
1411
1412 assert!(agent.supports_file_type(&PathBuf::from("test.rs")));
1413 assert!(agent.supports_file_type(&PathBuf::from("test.py")));
1414 assert!(agent.supports_file_type(&PathBuf::from("test.js")));
1415 assert!(!agent.supports_file_type(&PathBuf::from("test.txt")));
1416 }
1417
1418 #[test]
1419 fn test_finding_severity_ordering() {
1420 assert!(Severity::Critical > Severity::High);
1421 assert!(Severity::High > Severity::Medium);
1422 assert!(Severity::Medium > Severity::Low);
1423 assert!(Severity::Low > Severity::Info);
1424 }
1425
1426 #[test]
1427 fn test_agent_result_creation() {
1428 let result = AgentResult {
1429 agent_name: "test-agent".to_string(),
1430 status: AgentStatus::Completed,
1431 findings: vec![],
1432 analyzed_files: vec![],
1433 modified_files: vec![],
1434 execution_time: Duration::from_secs(30),
1435 summary: "Test completed".to_string(),
1436 metrics: HashMap::new(),
1437 };
1438
1439 assert_eq!(result.agent_name, "test-agent");
1440 assert_eq!(result.status, AgentStatus::Completed);
1441 assert_eq!(result.execution_time, Duration::from_secs(30));
1442 }
1443}