oxi_agent/tools/
review_tool.rs1use super::{AgentTool, AgentToolResult, ToolContext, ToolError, ToolExecutionMode, ToolTier};
8use async_trait::async_trait;
9use serde_json::{Value, json};
10use tokio::sync::oneshot;
11
12pub struct ReviewTool;
14
15#[async_trait]
16impl AgentTool for ReviewTool {
17 fn name(&self) -> &str {
18 "review"
19 }
20
21 fn label(&self) -> &str {
22 "Review"
23 }
24
25 fn description(&self) -> &str {
26 concat!(
27 "Request code review with focus areas and finding priorities. ",
28 "Specify files to review, the focus area (security, performance, ",
29 "correctness, style, or all), and the finding priority level. ",
30 "Use the subagent tool to dispatch the actual review."
31 )
32 }
33
34 fn essential(&self) -> bool {
35 false
36 }
37
38 fn parameters_schema(&self) -> Value {
39 json!({
40 "type": "object",
41 "properties": {
42 "files": {
43 "type": "array",
44 "items": {"type": "string"},
45 "description": "Files to review (paths relative to workspace root)."
46 },
47 "context": {
48 "type": "string",
49 "description": "Additional context for the review (e.g., recent changes, known issues)."
50 },
51 "focus": {
52 "type": "string",
53 "enum": ["security", "performance", "correctness", "style", "all"],
54 "description": "Focus area for the review."
55 },
56 "priority": {
57 "type": "string",
58 "enum": ["P0", "P1", "P2", "P3"],
59 "description": "Finding priority threshold. P0=critical, P3=informational."
60 }
61 }
62 })
63 }
64
65 fn intent(&self) -> Option<&str> {
66 Some("Request code review")
67 }
68
69 fn execution_mode(&self) -> ToolExecutionMode {
70 ToolExecutionMode::SequentialOnly
71 }
72
73 fn tool_tier(&self) -> ToolTier {
74 ToolTier::Read
75 }
76
77 async fn execute(
78 &self,
79 _tool_call_id: &str,
80 params: Value,
81 _signal: Option<oneshot::Receiver<()>>,
82 _ctx: &ToolContext,
83 ) -> Result<AgentToolResult, ToolError> {
84 let files: Vec<String> = params
85 .get("files")
86 .and_then(|v| v.as_array())
87 .map(|a| {
88 a.iter()
89 .filter_map(|v| v.as_str().map(String::from))
90 .collect()
91 })
92 .unwrap_or_default();
93
94 let context = params
95 .get("context")
96 .and_then(|v| v.as_str())
97 .map(String::from);
98
99 let focus = params
100 .get("focus")
101 .and_then(|v| v.as_str())
102 .unwrap_or("all");
103
104 let priority = params
105 .get("priority")
106 .and_then(|v| v.as_str())
107 .unwrap_or("P2");
108
109 let mut lines = vec!["## Code Review Request".to_string()];
110 lines.push(format!("- Focus: {}", focus));
111 lines.push(format!("- Priority threshold: {}", priority));
112
113 if files.is_empty() {
114 lines.push("\nFiles: (no specific files — review recent changes)".to_string());
115 } else {
116 lines.push(format!("\nFiles ({}):", files.len()));
117 for file in &files {
118 lines.push(format!(" - {}", file));
119 }
120 }
121
122 if let Some(ref ctx) = context {
123 lines.push(format!("\nContext:\n{}", ctx));
124 }
125
126 lines.push("\n## How to proceed".to_string());
127 lines.push("To perform the actual review, use the subagent tool:".to_string());
128 lines.push(
129 "1. Create a reviewer agent with `subagent` tool and agent: 'reviewer'".to_string(),
130 );
131 lines.push("2. Pass the review files and focus area in the task description".to_string());
132 lines.push("3. The reviewer will return findings with P0-P3 priorities".to_string());
133
134 Ok(AgentToolResult::success(lines.join("\n")))
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[tokio::test]
143 async fn test_review_basic() {
144 let tool = ReviewTool;
145 let params = json!({
146 "files": ["src/auth.rs", "src/api.rs"],
147 "focus": "security",
148 "priority": "P0"
149 });
150 let result = tool
151 .execute("id", params, None, &ToolContext::default())
152 .await
153 .unwrap();
154 assert!(result.success);
155 assert!(result.output.contains("security"));
156 assert!(result.output.contains("P0"));
157 assert!(result.output.contains("src/auth.rs"));
158 }
159
160 #[tokio::test]
161 async fn test_review_all_focus() {
162 let tool = ReviewTool;
163 let params = json!({
164 "context": "Recent refactor of the database layer",
165 "focus": "all"
166 });
167 let result = tool
168 .execute("id", params, None, &ToolContext::default())
169 .await
170 .unwrap();
171 assert!(result.success);
172 assert!(result.output.contains("Review Request"));
173 assert!(result.output.contains("subagent"));
174 }
175}