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 anyhow::Result;
8use async_trait::async_trait;
9use serde_json::{Value, json};
10
11/// RLM Tool - Invoke the Recursive Language Model subsystem
12/// for analyzing large codebases that exceed the context window
13pub struct RlmTool {
14    max_chunk_size: usize,
15}
16
17impl Default for RlmTool {
18    fn default() -> Self {
19        Self::new()
20    }
21}
22
23impl RlmTool {
24    pub fn new() -> Self {
25        Self {
26            max_chunk_size: 8192,
27        }
28    }
29
30    #[allow(dead_code)]
31    pub fn with_chunk_size(max_chunk_size: usize) -> Self {
32        Self { max_chunk_size }
33    }
34}
35
36#[async_trait]
37impl Tool for RlmTool {
38    fn id(&self) -> &str {
39        "rlm"
40    }
41
42    fn name(&self) -> &str {
43        "RLM"
44    }
45
46    fn description(&self) -> &str {
47        "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)."
48    }
49
50    fn parameters(&self) -> Value {
51        json!({
52            "type": "object",
53            "properties": {
54                "action": {
55                    "type": "string",
56                    "description": "Action: 'analyze' (deep analysis), 'summarize' (generate summary), 'search' (semantic search)",
57                    "enum": ["analyze", "summarize", "search"]
58                },
59                "query": {
60                    "type": "string",
61                    "description": "The question or query to answer (for analyze/search)"
62                },
63                "paths": {
64                    "type": "array",
65                    "items": {"type": "string"},
66                    "description": "File or directory paths to process"
67                },
68                "content": {
69                    "type": "string",
70                    "description": "Direct content to analyze (alternative to paths)"
71                },
72                "max_depth": {
73                    "type": "integer",
74                    "description": "Maximum recursion depth (default: 3)",
75                    "default": 3
76                }
77            },
78            "required": ["action"]
79        })
80    }
81
82    async fn execute(&self, args: Value) -> Result<ToolResult> {
83        let action = args["action"]
84            .as_str()
85            .ok_or_else(|| anyhow::anyhow!("action is required"))?;
86
87        let query = args["query"].as_str().unwrap_or("");
88        let paths: Vec<&str> = args["paths"]
89            .as_array()
90            .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
91            .unwrap_or_default();
92        let content = args["content"].as_str();
93        let max_depth = args["max_depth"].as_u64().unwrap_or(3) as usize;
94
95        match action {
96            "analyze" => {
97                if query.is_empty() {
98                    return Ok(ToolResult::error("query is required for 'analyze' action"));
99                }
100
101                // Collect content from paths or direct content
102                let all_content = if let Some(c) = content {
103                    c.to_string()
104                } else if !paths.is_empty() {
105                    let mut collected = String::new();
106                    for path in &paths {
107                        match tokio::fs::read_to_string(path).await {
108                            Ok(c) => {
109                                collected.push_str(&format!("=== {} ===\n{}\n\n", path, c));
110                            }
111                            Err(e) => {
112                                collected.push_str(&format!("=== {} (error: {}) ===\n\n", path, e));
113                            }
114                        }
115                    }
116                    collected
117                } else {
118                    return Ok(ToolResult::error("Either 'paths' or 'content' is required"));
119                };
120
121                // For now, return a chunked analysis placeholder
122                // Full implementation would invoke the RLM subsystem
123                let chunks = self.chunk_content(&all_content);
124                let first_chunk_preview = chunks
125                    .first()
126                    .map(|chunk| truncate_with_ellipsis(chunk, 500))
127                    .unwrap_or_default();
128                let output = format!(
129                    "RLM Analysis\n\
130                    Query: {}\n\
131                    Paths: {:?}\n\
132                    Content size: {} bytes\n\
133                    Chunks: {}\n\
134                    Max depth: {}\n\n\
135                    [Full RLM processing would analyze each chunk and synthesize results]\n\n\
136                    Content preview (first chunk):\n{}",
137                    query,
138                    paths,
139                    all_content.len(),
140                    chunks.len(),
141                    max_depth,
142                    first_chunk_preview
143                );
144
145                Ok(ToolResult::success(output))
146            }
147            "summarize" => {
148                if paths.is_empty() && content.is_none() {
149                    return Ok(ToolResult::error("Either 'paths' or 'content' is required"));
150                }
151
152                let all_content = if let Some(c) = content {
153                    c.to_string()
154                } else {
155                    let mut collected = String::new();
156                    for path in &paths {
157                        match tokio::fs::read_to_string(path).await {
158                            Ok(c) => collected.push_str(&c),
159                            Err(e) => {
160                                collected.push_str(&format!("[Error reading {}: {}]\n", path, e))
161                            }
162                        }
163                    }
164                    collected
165                };
166
167                let chunks = self.chunk_content(&all_content);
168                let output = format!(
169                    "RLM Summary\n\
170                    Paths: {:?}\n\
171                    Content size: {} bytes\n\
172                    Chunks: {}\n\n\
173                    [Full RLM would summarize each chunk and combine summaries]",
174                    paths,
175                    all_content.len(),
176                    chunks.len()
177                );
178
179                Ok(ToolResult::success(output))
180            }
181            "search" => {
182                if query.is_empty() {
183                    return Ok(ToolResult::error("query is required for 'search' action"));
184                }
185
186                let output = format!(
187                    "RLM Semantic Search\n\
188                    Query: {}\n\
189                    Paths: {:?}\n\n\
190                    [Full RLM would perform semantic search across chunks]",
191                    query, paths
192                );
193
194                Ok(ToolResult::success(output))
195            }
196            _ => Ok(ToolResult::error(format!(
197                "Unknown action: {}. Use 'analyze', 'summarize', or 'search'.",
198                action
199            ))),
200        }
201    }
202}
203
204impl RlmTool {
205    fn chunk_content(&self, content: &str) -> Vec<String> {
206        let mut chunks = Vec::new();
207        let lines: Vec<&str> = content.lines().collect();
208        let mut current_chunk = String::new();
209
210        for line in lines {
211            if current_chunk.len() + line.len() + 1 > self.max_chunk_size
212                && !current_chunk.is_empty()
213            {
214                chunks.push(current_chunk);
215                current_chunk = String::new();
216            }
217            current_chunk.push_str(line);
218            current_chunk.push('\n');
219        }
220
221        if !current_chunk.is_empty() {
222            chunks.push(current_chunk);
223        }
224
225        chunks
226    }
227}
228
229fn truncate_with_ellipsis(value: &str, max_chars: usize) -> String {
230    if max_chars == 0 {
231        return String::new();
232    }
233
234    let mut chars = value.chars();
235    let mut output = String::new();
236    for _ in 0..max_chars {
237        if let Some(ch) = chars.next() {
238            output.push(ch);
239        } else {
240            return value.to_string();
241        }
242    }
243
244    if chars.next().is_some() {
245        format!("{output}...")
246    } else {
247        output
248    }
249}