codetether_agent/tool/
rlm.rs1use super::{Tool, ToolResult};
7use anyhow::Result;
8use async_trait::async_trait;
9use serde_json::{Value, json};
10
11pub 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 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 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}