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    config: RlmConfig,
21}
22
23impl RlmTool {
24    /// Build an `RlmTool` backed by `provider`/`model`.
25    ///
26    /// The `config` argument is used for threshold, iteration limits,
27    /// and — once subcall routing lands — sub-LLM model selection.
28    /// Callers that do not care about RLM tuning can pass
29    /// [`RlmConfig::default`].
30    pub fn new(provider: Arc<dyn Provider>, model: String, config: RlmConfig) -> Self {
31        Self {
32            provider,
33            model,
34            config,
35        }
36    }
37}
38
39#[async_trait]
40impl Tool for RlmTool {
41    fn id(&self) -> &str {
42        "rlm"
43    }
44
45    fn name(&self) -> &str {
46        "RLM"
47    }
48
49    fn description(&self) -> &str {
50        "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)."
51    }
52
53    fn parameters(&self) -> Value {
54        json!({
55            "type": "object",
56            "properties": {
57                "action": {
58                    "type": "string",
59                    "description": "Action: 'analyze' (deep analysis), 'summarize' (generate summary), 'search' (semantic search)",
60                    "enum": ["analyze", "summarize", "search"]
61                },
62                "query": {
63                    "type": "string",
64                    "description": "The question or query to answer (for analyze/search)"
65                },
66                "paths": {
67                    "type": "array",
68                    "items": {"type": "string"},
69                    "description": "File or directory paths to process"
70                },
71                "content": {
72                    "type": "string",
73                    "description": "Direct content to analyze (alternative to paths)"
74                },
75                "max_depth": {
76                    "type": "integer",
77                    "description": "Maximum recursion depth (default: 3)",
78                    "default": 3
79                }
80            },
81            "required": ["action"]
82        })
83    }
84
85    async fn execute(&self, args: Value) -> Result<ToolResult> {
86        let action = args["action"]
87            .as_str()
88            .ok_or_else(|| anyhow::anyhow!("action is required"))?;
89
90        let query = args["query"].as_str().unwrap_or("");
91        let paths: Vec<&str> = args["paths"]
92            .as_array()
93            .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
94            .unwrap_or_default();
95        let content = args["content"].as_str();
96
97        match action {
98            "analyze" | "summarize" | "search" => {
99                if action != "summarize" && query.is_empty() {
100                    return Ok(ToolResult::error(format!(
101                        "query is required for '{}' action",
102                        action
103                    )));
104                }
105
106                // Collect content from paths or direct content
107                let all_content = if let Some(c) = content {
108                    c.to_string()
109                } else if !paths.is_empty() {
110                    let mut collected = String::new();
111                    for path in &paths {
112                        match tokio::fs::read_to_string(path).await {
113                            Ok(c) => {
114                                collected.push_str(&format!("=== {} ===\n{}\n\n", path, c));
115                            }
116                            Err(e) => {
117                                collected.push_str(&format!("=== {} (error: {}) ===\n\n", path, e));
118                            }
119                        }
120                    }
121                    collected
122                } else {
123                    return Ok(ToolResult::error("Either 'paths' or 'content' is required"));
124                };
125
126                let input_tokens = RlmChunker::estimate_tokens(&all_content);
127                let effective_query = if query.is_empty() {
128                    format!("Summarize the content from: {:?}", paths)
129                } else {
130                    query.to_string()
131                };
132
133                // Use RlmRouter::auto_process for real analysis
134                let auto_ctx = AutoProcessContext {
135                    tool_id: action,
136                    tool_args: json!({ "query": effective_query, "paths": paths }),
137                    session_id: "rlm-tool",
138                    abort: None,
139                    on_progress: None,
140                    provider: Arc::clone(&self.provider),
141                    model: self.model.clone(),
142                    bus: None,
143                    trace_id: None,
144                    subcall_provider: None,
145                    subcall_model: None,
146                };
147                let config = self.config.clone();
148
149                match RlmRouter::auto_process(&all_content, auto_ctx, &config).await {
150                    Ok(result) => {
151                        let output = format!(
152                            "RLM {} complete ({} → {} tokens, {} iterations)\n\n{}",
153                            action,
154                            result.stats.input_tokens,
155                            result.stats.output_tokens,
156                            result.stats.iterations,
157                            result.processed
158                        );
159                        Ok(ToolResult::success(output))
160                    }
161                    Err(e) => {
162                        // Fallback to smart truncation
163                        tracing::warn!(error = %e, "RLM auto_process failed, falling back to truncation");
164                        let (truncated, _, _) = RlmRouter::smart_truncate(
165                            &all_content,
166                            action,
167                            &json!({}),
168                            input_tokens.min(8000),
169                        );
170                        Ok(ToolResult::success(format!(
171                            "RLM {} (fallback mode - auto_process failed: {})\n\n{}",
172                            action, e, truncated
173                        )))
174                    }
175                }
176            }
177            _ => Ok(ToolResult::error(format!(
178                "Unknown action: {}. Use 'analyze', 'summarize', or 'search'.",
179                action
180            ))),
181        }
182    }
183}