1use 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
14use super::glob::FileType;
16use super::glob::GlobTool;
17use super::plan::PlanTool;
18use super::think::ThinkTool;
19use super::tree::TreeTool;
20
21pub 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 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 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
71pub fn adapt_plan_tool(input: Value) -> Result<ToolOutput, ToolError> {
73 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 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 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 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 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 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
143pub 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 let base_path = PathBuf::from(path);
153 let tool = GlobTool::new(base_path);
154
155 let result = tool
157 .glob(pattern)
158 .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
159
160 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 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
206pub 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 Ok(ToolOutput::success(
218 json!({
219 "results": [],
220 "message": "Search functionality requires index initialization"
221 }),
222 "Search requires index setup",
223 ))
224}
225
226pub 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 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 let tool = TreeTool::new(IntelligenceLevel::Medium)
240 .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
241
242 let parse_result = tool
244 .parse_file(PathBuf::from(file_path))
245 .await
246 .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
247
248 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 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
274pub 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 let content = std::fs::read_to_string(file).map_err(ToolError::Io)?;
290
291 if !content.contains(old_text) {
293 return Err(ToolError::InvalidInput("old_text not found in file".into()));
294 }
295
296 let new_content = content.replace(old_text, new_text);
298
299 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
313pub 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 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
364pub 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 let _config = super::grep_simple::GrepConfig::default();
375
376 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
388pub 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 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}