Skip to main content

codetether_agent/tool/
rlm.rs

1//! RLM tool: Recursive Language Model for large context analysis
2//!
3//! This tool invokes the RLM subsystem to process large codebases that exceed
4//! the context window. It chunks, routes, and synthesizes results.
5
6use super::{Tool, ToolResult};
7use crate::provider::Provider;
8use crate::rlm::router::AutoProcessContext;
9use crate::rlm::{RlmChunker, RlmConfig, RlmRouter};
10use anyhow::Result;
11use async_trait::async_trait;
12use serde_json::{Value, json};
13use std::sync::Arc;
14
15/// RLM Tool - Invoke the Recursive Language Model subsystem
16/// for analyzing large codebases that exceed the context window
17pub struct RlmTool {
18    provider: Arc<dyn Provider>,
19    model: String,
20}
21
22impl RlmTool {
23    pub fn new(provider: Arc<dyn Provider>, model: String) -> Self {
24        Self { provider, model }
25    }
26}
27
28#[async_trait]
29impl Tool for RlmTool {
30    fn id(&self) -> &str {
31        "rlm"
32    }
33
34    fn name(&self) -> &str {
35        "RLM"
36    }
37
38    fn description(&self) -> &str {
39        "Recursive Language Model for processing large codebases. Use this when you need to analyze files or content that exceeds the context window. RLM chunks the content, processes each chunk, and synthesizes results. Actions: 'analyze' (analyze large content), 'summarize' (summarize large files), 'search' (semantic search across large codebase)."
40    }
41
42    fn parameters(&self) -> Value {
43        json!({
44            "type": "object",
45            "properties": {
46                "action": {
47                    "type": "string",
48                    "description": "Action: 'analyze' (deep analysis), 'summarize' (generate summary), 'search' (semantic search)",
49                    "enum": ["analyze", "summarize", "search"]
50                },
51                "query": {
52                    "type": "string",
53                    "description": "The question or query to answer (for analyze/search)"
54                },
55                "paths": {
56                    "type": "array",
57                    "items": {"type": "string"},
58                    "description": "File or directory paths to process"
59                },
60                "content": {
61                    "type": "string",
62                    "description": "Direct content to analyze (alternative to paths)"
63                },
64                "max_depth": {
65                    "type": "integer",
66                    "description": "Maximum recursion depth (default: 3)",
67                    "default": 3
68                }
69            },
70            "required": ["action"]
71        })
72    }
73
74    async fn execute(&self, args: Value) -> Result<ToolResult> {
75        let action = args["action"]
76            .as_str()
77            .ok_or_else(|| anyhow::anyhow!("action is required"))?;
78
79        let query = args["query"].as_str().unwrap_or("");
80        let paths: Vec<&str> = args["paths"]
81            .as_array()
82            .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
83            .unwrap_or_default();
84        let content = args["content"].as_str();
85
86        match action {
87            "analyze" | "summarize" | "search" => {
88                if action != "summarize" && query.is_empty() {
89                    return Ok(ToolResult::error(format!(
90                        "query is required for '{}' action",
91                        action
92                    )));
93                }
94
95                // Collect content from paths or direct content
96                let all_content = if let Some(c) = content {
97                    c.to_string()
98                } else if !paths.is_empty() {
99                    let mut collected = String::new();
100                    for path in &paths {
101                        match tokio::fs::read_to_string(path).await {
102                            Ok(c) => {
103                                collected.push_str(&format!("=== {} ===\n{}\n\n", path, c));
104                            }
105                            Err(e) => {
106                                collected.push_str(&format!("=== {} (error: {}) ===\n\n", path, e));
107                            }
108                        }
109                    }
110                    collected
111                } else {
112                    return Ok(ToolResult::error("Either 'paths' or 'content' is required"));
113                };
114
115                let input_tokens = RlmChunker::estimate_tokens(&all_content);
116                let effective_query = if query.is_empty() {
117                    format!("Summarize the content from: {:?}", paths)
118                } else {
119                    query.to_string()
120                };
121
122                // Use RlmRouter::auto_process for real analysis
123                let auto_ctx = AutoProcessContext {
124                    tool_id: action,
125                    tool_args: json!({ "query": effective_query, "paths": paths }),
126                    session_id: "rlm-tool",
127                    abort: None,
128                    on_progress: None,
129                    provider: Arc::clone(&self.provider),
130                    model: self.model.clone(),
131                };
132                let config = RlmConfig::default();
133
134                match RlmRouter::auto_process(&all_content, auto_ctx, &config).await {
135                    Ok(result) => {
136                        let output = format!(
137                            "RLM {} complete ({} → {} tokens, {} iterations)\n\n{}",
138                            action,
139                            result.stats.input_tokens,
140                            result.stats.output_tokens,
141                            result.stats.iterations,
142                            result.processed
143                        );
144                        Ok(ToolResult::success(output))
145                    }
146                    Err(e) => {
147                        // Fallback to smart truncation
148                        tracing::warn!(error = %e, "RLM auto_process failed, falling back to truncation");
149                        let (truncated, _, _) = RlmRouter::smart_truncate(
150                            &all_content,
151                            action,
152                            &json!({}),
153                            input_tokens.min(8000),
154                        );
155                        Ok(ToolResult::success(format!(
156                            "RLM {} (fallback mode - auto_process failed: {})\n\n{}",
157                            action, e, truncated
158                        )))
159                    }
160                }
161            }
162            _ => Ok(ToolResult::error(format!(
163                "Unknown action: {}. Use 'analyze', 'summarize', or 'search'.",
164                action
165            ))),
166        }
167    }
168}