agcodex_core/tools/
adapters.rs

1//! Tool adapters to bridge existing tools with the unified registry
2//!
3//! These adapters wrap existing tool implementations to work with the
4//! simple Value -> ToolOutput interface.
5
6use super::registry::ToolError;
7use super::registry::ToolOutput;
8use crate::subagents::IntelligenceLevel;
9use serde_json::Value;
10use serde_json::json;
11use std::path::PathBuf;
12use tokio::runtime::Runtime;
13
14// Import existing tools
15use super::glob::FileType;
16use super::glob::GlobTool;
17use super::plan::PlanTool;
18use super::think::ThinkTool;
19use super::tree::TreeTool;
20
21/// Adapter for the think tool
22pub fn adapt_think_tool(input: Value) -> Result<ToolOutput, ToolError> {
23    let problem = input["problem"]
24        .as_str()
25        .ok_or_else(|| ToolError::InvalidInput("missing 'problem' field".into()))?;
26
27    let language = input["language"].as_str();
28    let context = input["context"].as_str();
29
30    // Use the code-specific version if language is provided
31    let result = if language.is_some() || context.is_some() {
32        let code_result = ThinkTool::think_about_code(problem, language, context)
33            .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
34
35        json!({
36            "problem_type": format!("{:?}", code_result.problem_type),
37            "complexity": format!("{:?}", code_result.complexity),
38            "steps": code_result.steps.iter().map(|s| json!({
39                "number": s.step_number,
40                "thought": s.thought,
41                "reasoning": s.reasoning,
42            })).collect::<Vec<_>>(),
43            "conclusion": code_result.conclusion,
44            "confidence": code_result.confidence,
45            "recommended_action": code_result.recommended_action,
46            "affected_files": code_result.affected_files,
47        })
48    } else {
49        // Use basic thinking
50        let basic_result =
51            ThinkTool::think(problem).map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
52
53        json!({
54            "steps": basic_result.steps.iter().map(|s| json!({
55                "number": s.step_number,
56                "thought": s.thought,
57                "reasoning": s.reasoning,
58            })).collect::<Vec<_>>(),
59            "conclusion": basic_result.conclusion,
60            "confidence": basic_result.confidence,
61        })
62    };
63
64    let confidence = result["confidence"].as_f64().unwrap_or(0.0);
65    Ok(ToolOutput::success(
66        result,
67        format!("Analyzed problem with {:.2} confidence", confidence),
68    ))
69}
70
71/// Adapter for the plan tool
72pub fn adapt_plan_tool(input: Value) -> Result<ToolOutput, ToolError> {
73    // The plan tool expects a goal string, which can be either 'goal' or 'description'
74    let goal = input["goal"]
75        .as_str()
76        .or_else(|| input["description"].as_str())
77        .ok_or_else(|| ToolError::InvalidInput("missing 'goal' or 'description' field".into()))?;
78
79    // Optionally append constraints to the goal if provided
80    let constraints = input["constraints"]
81        .as_array()
82        .map(|arr| {
83            arr.iter()
84                .filter_map(|v| v.as_str())
85                .map(|s| s.to_string())
86                .collect::<Vec<_>>()
87        })
88        .unwrap_or_default();
89
90    // If constraints are provided, append them to the goal description
91    let full_goal = if constraints.is_empty() {
92        goal.to_string()
93    } else {
94        format!("{} with constraints: {}", goal, constraints.join(", "))
95    };
96
97    let tool = PlanTool::new();
98    let result = tool
99        .plan(&full_goal)
100        .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
101
102    // Convert tasks to JSON format
103    let tasks_json: Vec<Value> = result
104        .tasks
105        .iter()
106        .map(|task| {
107            json!({
108                "id": task.id.to_string(),
109                "description": task.description,
110                "depends_on": task.depends_on.iter().map(|id| id.to_string()).collect::<Vec<_>>(),
111                "can_parallelize": task.can_parallelize,
112            })
113        })
114        .collect();
115
116    // Convert parallel groups to JSON format
117    let parallel_groups_json: Vec<Value> = result
118        .parallel_groups
119        .iter()
120        .map(|group| json!(group.iter().map(|id| id.to_string()).collect::<Vec<_>>()))
121        .collect();
122
123    // Convert dependency graph to JSON object
124    let mut dependency_graph_json = serde_json::Map::new();
125    for (task_id, deps) in result.dependency_graph.iter() {
126        dependency_graph_json.insert(
127            task_id.to_string(),
128            json!(deps.iter().map(|id| id.to_string()).collect::<Vec<_>>()),
129        );
130    }
131
132    Ok(ToolOutput::success(
133        json!({
134            "tasks": tasks_json,
135            "dependency_graph": dependency_graph_json,
136            "parallel_groups": parallel_groups_json,
137            "estimated_complexity": format!("{:?}", result.estimated_complexity),
138        }),
139        format!("Created plan with {} tasks", result.tasks.len()),
140    ))
141}
142
143/// Adapter for the glob tool
144pub fn adapt_glob_tool(input: Value) -> Result<ToolOutput, ToolError> {
145    let pattern = input["pattern"]
146        .as_str()
147        .ok_or_else(|| ToolError::InvalidInput("missing 'pattern' field".into()))?;
148
149    let path = input["path"].as_str().unwrap_or(".");
150
151    // Create GlobTool with base directory
152    let base_path = PathBuf::from(path);
153    let tool = GlobTool::new(base_path);
154
155    // Use the glob method to search for files
156    let result = tool
157        .glob(pattern)
158        .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
159
160    // Map the FileMatch results to JSON
161    let files: Vec<Value> = result
162        .result
163        .iter()
164        .map(|m| {
165            json!({
166                "path": m.path.display().to_string(),
167                "relative_path": m.relative_path.display().to_string(),
168                "size": m.size,
169                "extension": m.extension,
170                "file_type": match m.file_type {
171                    FileType::File => "file",
172                    FileType::Directory => "directory",
173                    FileType::Symlink => "symlink",
174                    FileType::Other => "other",
175                },
176                "content_category": format!("{:?}", m.content_category),
177                "executable": m.executable,
178                "estimated_lines": m.estimated_lines,
179            })
180        })
181        .collect();
182
183    // Calculate search duration from timestamps
184    let duration_ms = result
185        .metadata
186        .completed_at
187        .duration_since(result.metadata.started_at)
188        .map(|d| d.as_millis() as u64)
189        .unwrap_or(0);
190
191    Ok(ToolOutput::success(
192        json!({
193            "matches": files,
194            "total_files": result.result.len(),
195            "search_duration_ms": duration_ms,
196            "summary": result.summary,
197        }),
198        format!(
199            "Found {} files matching pattern '{}'",
200            result.result.len(),
201            pattern
202        ),
203    ))
204}
205
206/// Adapter for the search/index tool
207pub fn adapt_search_tool(input: Value) -> Result<ToolOutput, ToolError> {
208    let _query_text = input["query"]
209        .as_str()
210        .ok_or_else(|| ToolError::InvalidInput("missing 'query' field".into()))?;
211
212    let _limit = input["limit"].as_u64().unwrap_or(10) as usize;
213    let _path = input["path"].as_str();
214
215    // For now, return a placeholder since IndexTool requires setup
216    // In production, this would use the actual IndexTool
217    Ok(ToolOutput::success(
218        json!({
219            "results": [],
220            "message": "Search functionality requires index initialization"
221        }),
222        "Search requires index setup",
223    ))
224}
225
226/// Adapter for the tree tool
227pub fn adapt_tree_tool(input: Value) -> Result<ToolOutput, ToolError> {
228    let file_path = input["file"]
229        .as_str()
230        .ok_or_else(|| ToolError::InvalidInput("missing 'file' field".into()))?;
231
232    // Since this is a synchronous adapter, we need to use a blocking approach
233    // In production, this should be called from an async context
234    let rt = Runtime::new()
235        .map_err(|e| ToolError::ExecutionFailed(format!("Failed to create runtime: {}", e)))?;
236
237    let result = rt.block_on(async {
238        // Create the tool with default intelligence level
239        let tool = TreeTool::new(IntelligenceLevel::Medium)
240            .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
241
242        // Parse the file (language will be auto-detected)
243        let parse_result = tool
244            .parse_file(PathBuf::from(file_path))
245            .await
246            .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
247
248        // Extract information from ParsedAst
249        let language_str = parse_result.language.as_str();
250        let node_count = parse_result.node_count;
251        let has_errors = parse_result.has_errors();
252        let parse_time_ms = parse_result.parse_time.as_millis() as u64;
253
254        Ok::<_, ToolError>(json!({
255            "language": language_str,
256            "node_count": node_count,
257            "has_errors": has_errors,
258            "parse_time_ms": parse_time_ms,
259            "source_length": parse_result.source_code.len(),
260            "file": file_path,
261        }))
262    })?;
263
264    // Extract values before moving result
265    let node_count = result["node_count"].as_u64().unwrap_or(0);
266    let language = result["language"].as_str().unwrap_or("unknown").to_string();
267
268    Ok(ToolOutput::success(
269        result,
270        format!("Parsed {} file with {} nodes", language, node_count),
271    ))
272}
273
274/// Adapter for the edit tool (simple text replacement)
275pub fn adapt_edit_tool(input: Value) -> Result<ToolOutput, ToolError> {
276    let file = input["file"]
277        .as_str()
278        .ok_or_else(|| ToolError::InvalidInput("missing 'file' field".into()))?;
279
280    let old_text = input["old_text"]
281        .as_str()
282        .ok_or_else(|| ToolError::InvalidInput("missing 'old_text' field".into()))?;
283
284    let new_text = input["new_text"]
285        .as_str()
286        .ok_or_else(|| ToolError::InvalidInput("missing 'new_text' field".into()))?;
287
288    // Read file
289    let content = std::fs::read_to_string(file).map_err(ToolError::Io)?;
290
291    // Check if old_text exists
292    if !content.contains(old_text) {
293        return Err(ToolError::InvalidInput("old_text not found in file".into()));
294    }
295
296    // Replace text
297    let new_content = content.replace(old_text, new_text);
298
299    // Write file
300    std::fs::write(file, &new_content).map_err(ToolError::Io)?;
301
302    Ok(ToolOutput::success(
303        json!({
304            "file": file,
305            "changes": 1,
306            "old_text": old_text,
307            "new_text": new_text,
308        }),
309        format!("Edited {} successfully", file),
310    ))
311}
312
313/// Adapter for the patch tool (bulk operations)
314pub fn adapt_patch_tool(input: Value) -> Result<ToolOutput, ToolError> {
315    let operation = input["operation"]
316        .as_str()
317        .ok_or_else(|| ToolError::InvalidInput("missing 'operation' field".into()))?;
318
319    match operation {
320        "rename_symbol" => {
321            let old_name = input["old_name"]
322                .as_str()
323                .ok_or_else(|| ToolError::InvalidInput("missing 'old_name'".into()))?;
324            let new_name = input["new_name"]
325                .as_str()
326                .ok_or_else(|| ToolError::InvalidInput("missing 'new_name'".into()))?;
327
328            // Placeholder for actual implementation
329            Ok(ToolOutput::success(
330                json!({
331                    "operation": "rename_symbol",
332                    "old_name": old_name,
333                    "new_name": new_name,
334                    "message": "Symbol rename requires async runtime"
335                }),
336                "Symbol rename placeholder",
337            ))
338        }
339        "extract_function" => {
340            let file = input["file"]
341                .as_str()
342                .ok_or_else(|| ToolError::InvalidInput("missing 'file'".into()))?;
343            let function_name = input["function_name"]
344                .as_str()
345                .ok_or_else(|| ToolError::InvalidInput("missing 'function_name'".into()))?;
346
347            Ok(ToolOutput::success(
348                json!({
349                    "operation": "extract_function",
350                    "file": file,
351                    "function_name": function_name,
352                    "message": "Function extraction requires async runtime"
353                }),
354                "Function extraction placeholder",
355            ))
356        }
357        _ => Err(ToolError::InvalidInput(format!(
358            "unknown operation: {}",
359            operation
360        ))),
361    }
362}
363
364/// Adapter for the grep tool
365pub fn adapt_grep_tool(input: Value) -> Result<ToolOutput, ToolError> {
366    let pattern = input["pattern"]
367        .as_str()
368        .ok_or_else(|| ToolError::InvalidInput("missing 'pattern' field".into()))?;
369
370    let path = input["path"].as_str().unwrap_or(".");
371    let language = input["language"].as_str();
372
373    // Create default grep config - builder methods not yet implemented
374    let _config = super::grep_simple::GrepConfig::default();
375
376    // For now, return placeholder since GrepTool requires more setup
377    Ok(ToolOutput::success(
378        json!({
379            "pattern": pattern,
380            "path": path,
381            "language": language,
382            "message": "Grep functionality requires further implementation"
383        }),
384        "Grep search placeholder",
385    ))
386}
387
388/// Adapter for bash tool (command execution)
389pub fn adapt_bash_tool(input: Value) -> Result<ToolOutput, ToolError> {
390    let command = input["command"]
391        .as_str()
392        .ok_or_else(|| ToolError::InvalidInput("missing 'command' field".into()))?;
393
394    // Safety check - only allow read-only commands in this adapter
395    let read_only_commands = ["ls", "pwd", "echo", "date", "whoami", "uname"];
396    let first_word = command.split_whitespace().next().unwrap_or("");
397
398    if !read_only_commands.contains(&first_word) {
399        return Err(ToolError::InvalidInput(format!(
400            "Command '{}' not allowed in safe mode",
401            first_word
402        )));
403    }
404
405    let output = std::process::Command::new("sh")
406        .arg("-c")
407        .arg(command)
408        .output()
409        .map_err(ToolError::Io)?;
410
411    Ok(ToolOutput::success(
412        json!({
413            "command": command,
414            "stdout": String::from_utf8_lossy(&output.stdout).to_string(),
415            "stderr": String::from_utf8_lossy(&output.stderr).to_string(),
416            "success": output.status.success(),
417        }),
418        format!("Executed command: {}", command),
419    ))
420}
421
422#[cfg(test)]
423mod tests {
424    use super::*;
425
426    #[test]
427    fn test_adapt_think_basic() {
428        let input = json!({
429            "problem": "How to implement a cache?"
430        });
431
432        let result = adapt_think_tool(input);
433        assert!(result.is_ok());
434
435        let output = result.unwrap();
436        assert!(output.success);
437        assert!(output.result["steps"].is_array());
438    }
439
440    #[test]
441    fn test_adapt_think_with_language() {
442        let input = json!({
443            "problem": "Fix null pointer exception",
444            "language": "java",
445            "context": "UserService.java"
446        });
447
448        let result = adapt_think_tool(input);
449        assert!(result.is_ok());
450
451        let output = result.unwrap();
452        assert!(output.success);
453        assert!(output.result["problem_type"].is_string());
454    }
455
456    #[test]
457    fn test_adapt_glob() {
458        let input = json!({
459            "pattern": "*.rs",
460            "path": ".",
461            "file_type": "file"
462        });
463
464        let result = adapt_glob_tool(input);
465        assert!(result.is_ok());
466    }
467
468    #[test]
469    fn test_adapt_bash_safe() {
470        let input = json!({
471            "command": "echo hello"
472        });
473
474        let result = adapt_bash_tool(input);
475        assert!(result.is_ok());
476
477        let output = result.unwrap();
478        assert!(output.success);
479        assert_eq!(output.result["stdout"].as_str().unwrap().trim(), "hello");
480    }
481
482    #[test]
483    fn test_adapt_bash_unsafe() {
484        let input = json!({
485            "command": "rm -rf /"
486        });
487
488        let result = adapt_bash_tool(input);
489        assert!(result.is_err());
490
491        if let Err(ToolError::InvalidInput(msg)) = result {
492            assert!(msg.contains("not allowed"));
493        } else {
494            panic!("Expected InvalidInput error");
495        }
496    }
497}