codetether_agent/tool/
rlm.rs1use 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
15pub struct RlmTool {
18 provider: Arc<dyn Provider>,
19 model: String,
20 config: RlmConfig,
21}
22
23impl RlmTool {
24 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 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 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 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}