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::rlm::{RlmChunker, RlmConfig, RlmRouter};
8use crate::rlm::router::AutoProcessContext;
9use crate::provider::Provider;
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", action
91                    )));
92                }
93
94                // Collect content from paths or direct content
95                let all_content = if let Some(c) = content {
96                    c.to_string()
97                } else if !paths.is_empty() {
98                    let mut collected = String::new();
99                    for path in &paths {
100                        match tokio::fs::read_to_string(path).await {
101                            Ok(c) => {
102                                collected.push_str(&format!("=== {} ===\n{}\n\n", path, c));
103                            }
104                            Err(e) => {
105                                collected.push_str(&format!("=== {} (error: {}) ===\n\n", path, e));
106                            }
107                        }
108                    }
109                    collected
110                } else {
111                    return Ok(ToolResult::error("Either 'paths' or 'content' is required"));
112                };
113
114                let input_tokens = RlmChunker::estimate_tokens(&all_content);
115                let effective_query = if query.is_empty() {
116                    format!("Summarize the content from: {:?}", paths)
117                } else {
118                    query.to_string()
119                };
120
121                // Use RlmRouter::auto_process for real analysis
122                let auto_ctx = AutoProcessContext {
123                    tool_id: action,
124                    tool_args: json!({ "query": effective_query, "paths": paths }),
125                    session_id: "rlm-tool",
126                    abort: None,
127                    on_progress: None,
128                    provider: Arc::clone(&self.provider),
129                    model: self.model.clone(),
130                };
131                let config = RlmConfig::default();
132
133                match RlmRouter::auto_process(&all_content, auto_ctx, &config).await {
134                    Ok(result) => {
135                        let output = format!(
136                            "RLM {} complete ({} → {} tokens, {} iterations)\n\n{}",
137                            action,
138                            result.stats.input_tokens,
139                            result.stats.output_tokens,
140                            result.stats.iterations,
141                            result.processed
142                        );
143                        Ok(ToolResult::success(output))
144                    }
145                    Err(e) => {
146                        // Fallback to smart truncation
147                        tracing::warn!(error = %e, "RLM auto_process failed, falling back to truncation");
148                        let (truncated, _, _) = RlmRouter::smart_truncate(
149                            &all_content, action, &json!({}), input_tokens.min(8000),
150                        );
151                        Ok(ToolResult::success(format!(
152                            "RLM {} (fallback mode - auto_process failed: {})\n\n{}",
153                            action, e, truncated
154                        )))
155                    }
156                }
157            }
158            _ => Ok(ToolResult::error(format!(
159                "Unknown action: {}. Use 'analyze', 'summarize', or 'search'.",
160                action
161            ))),
162        }
163    }
164}