1use crate::provider::ToolDefinition;
12
13pub fn rlm_tool_definitions() -> Vec<ToolDefinition> {
15 vec![
16 ToolDefinition {
17 name: "rlm_head".to_string(),
18 description: "Return the first N lines of the loaded context.".to_string(),
19 parameters: serde_json::json!({
20 "type": "object",
21 "properties": {
22 "n": {
23 "type": "integer",
24 "description": "Number of lines from the start (default: 10)"
25 }
26 },
27 "required": []
28 }),
29 },
30 ToolDefinition {
31 name: "rlm_tail".to_string(),
32 description: "Return the last N lines of the loaded context.".to_string(),
33 parameters: serde_json::json!({
34 "type": "object",
35 "properties": {
36 "n": {
37 "type": "integer",
38 "description": "Number of lines from the end (default: 10)"
39 }
40 },
41 "required": []
42 }),
43 },
44 ToolDefinition {
45 name: "rlm_grep".to_string(),
46 description: "Search the loaded context for lines matching a regex pattern. Returns matching lines with line numbers.".to_string(),
47 parameters: serde_json::json!({
48 "type": "object",
49 "properties": {
50 "pattern": {
51 "type": "string",
52 "description": "Regex pattern to search for"
53 }
54 },
55 "required": ["pattern"]
56 }),
57 },
58 ToolDefinition {
59 name: "rlm_count".to_string(),
60 description: "Count occurrences of a regex pattern in the loaded context.".to_string(),
61 parameters: serde_json::json!({
62 "type": "object",
63 "properties": {
64 "pattern": {
65 "type": "string",
66 "description": "Regex pattern to count"
67 }
68 },
69 "required": ["pattern"]
70 }),
71 },
72 ToolDefinition {
73 name: "rlm_slice".to_string(),
74 description: "Return a slice of the context by line range.".to_string(),
75 parameters: serde_json::json!({
76 "type": "object",
77 "properties": {
78 "start": {
79 "type": "integer",
80 "description": "Start line number (0-indexed)"
81 },
82 "end": {
83 "type": "integer",
84 "description": "End line number (exclusive)"
85 }
86 },
87 "required": ["start", "end"]
88 }),
89 },
90 ToolDefinition {
91 name: "rlm_llm_query".to_string(),
92 description: "Ask a focused sub-question about a portion of the context. Use this for semantic understanding of specific sections.".to_string(),
93 parameters: serde_json::json!({
94 "type": "object",
95 "properties": {
96 "query": {
97 "type": "string",
98 "description": "The question to answer about the context"
99 },
100 "context_slice": {
101 "type": "string",
102 "description": "Optional: specific text slice to analyze (if omitted, uses full context)"
103 }
104 },
105 "required": ["query"]
106 }),
107 },
108 ToolDefinition {
109 name: "rlm_final".to_string(),
110 description: "Return the final answer to the analysis query. Call this when you have gathered enough information to answer.".to_string(),
111 parameters: serde_json::json!({
112 "type": "object",
113 "properties": {
114 "answer": {
115 "type": "string",
116 "description": "The complete, detailed answer to the original query"
117 }
118 },
119 "required": ["answer"]
120 }),
121 },
122 ToolDefinition {
123 name: "rlm_ast_query".to_string(),
124 description: "Execute a tree-sitter AST query on the loaded context. Use this for structural code analysis (function signatures, struct fields, impl blocks).".to_string(),
125 parameters: serde_json::json!({
126 "type": "object",
127 "properties": {
128 "query": {
129 "type": "string",
130 "description": "Tree-sitter S-expression query (e.g., '(function_item name: (identifier) @name)')"
131 }
132 },
133 "required": ["query"]
134 }),
135 },
136 ]
137}
138
139pub enum RlmToolResult {
141 Output(String),
143 Final(String),
145}
146
147pub fn dispatch_tool_call(
152 name: &str,
153 arguments: &str,
154 repl: &mut super::repl::RlmRepl,
155) -> Option<RlmToolResult> {
156 let args: serde_json::Value = serde_json::from_str(arguments).unwrap_or_default();
157
158 match name {
159 "rlm_head" => {
160 let n = args.get("n").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
161 let output = repl.head(n).join("\n");
162 Some(RlmToolResult::Output(output))
163 }
164 "rlm_tail" => {
165 let n = args.get("n").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
166 let output = repl.tail(n).join("\n");
167 Some(RlmToolResult::Output(output))
168 }
169 "rlm_grep" => {
170 let pattern = args.get("pattern").and_then(|v| v.as_str()).unwrap_or("");
171 let matches = repl.grep(pattern);
172 let output = matches
173 .iter()
174 .map(|(i, line)| format!("{}:{}", i, line))
175 .collect::<Vec<_>>()
176 .join("\n");
177 if output.is_empty() {
178 Some(RlmToolResult::Output("(no matches)".to_string()))
179 } else {
180 Some(RlmToolResult::Output(output))
181 }
182 }
183 "rlm_count" => {
184 let pattern = args.get("pattern").and_then(|v| v.as_str()).unwrap_or("");
185 let count = repl.count(pattern);
186 Some(RlmToolResult::Output(count.to_string()))
187 }
188 "rlm_slice" => {
189 let start = args.get("start").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
190 let end = args.get("end").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
191 let output = repl.slice(start, end).to_string();
192 Some(RlmToolResult::Output(output))
193 }
194 "rlm_llm_query" => {
195 let query = args
198 .get("query")
199 .and_then(|v| v.as_str())
200 .unwrap_or("")
201 .to_string();
202 let context_slice = args
203 .get("context_slice")
204 .and_then(|v| v.as_str())
205 .map(|s| s.to_string());
206 let payload = serde_json::json!({
209 "__rlm_llm_query": true,
210 "query": query,
211 "context_slice": context_slice,
212 });
213 Some(RlmToolResult::Output(payload.to_string()))
214 }
215 "rlm_final" => {
216 let answer = args
217 .get("answer")
218 .and_then(|v| v.as_str())
219 .unwrap_or("")
220 .to_string();
221 Some(RlmToolResult::Final(answer))
222 }
223 "rlm_ast_query" => {
224 let query = args
225 .get("query")
226 .and_then(|v| v.as_str())
227 .unwrap_or("");
228
229 let mut oracle = super::oracle::TreeSitterOracle::new(repl.context().to_string());
231 match oracle.query(query) {
232 Ok(result) => {
233 let matches: Vec<serde_json::Value> = result.matches.iter().map(|m| {
235 serde_json::json!({
236 "line": m.line,
237 "column": m.column,
238 "captures": m.captures,
239 "text": m.text
240 })
241 }).collect();
242
243 let output = serde_json::json!({
244 "query": query,
245 "match_count": matches.len(),
246 "matches": matches
247 });
248
249 Some(RlmToolResult::Output(output.to_string()))
250 }
251 Err(e) => {
252 Some(RlmToolResult::Output(format!("AST query error: {}", e)))
253 }
254 }
255 }
256 _ => None, }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263 use crate::rlm::repl::{ReplRuntime, RlmRepl};
264
265 #[test]
266 fn tool_definitions_are_complete() {
267 let defs = rlm_tool_definitions();
268 assert_eq!(defs.len(), 8);
269 let names: Vec<&str> = defs.iter().map(|d| d.name.as_str()).collect();
270 assert!(names.contains(&"rlm_head"));
271 assert!(names.contains(&"rlm_tail"));
272 assert!(names.contains(&"rlm_grep"));
273 assert!(names.contains(&"rlm_count"));
274 assert!(names.contains(&"rlm_slice"));
275 assert!(names.contains(&"rlm_llm_query"));
276 assert!(names.contains(&"rlm_final"));
277 assert!(names.contains(&"rlm_ast_query"));
278 }
279
280 #[test]
281 fn dispatch_head() {
282 let ctx = "line 1\nline 2\nline 3\nline 4\nline 5".to_string();
283 let mut repl = RlmRepl::new(ctx, ReplRuntime::Rust);
284 let result = dispatch_tool_call("rlm_head", r#"{"n": 2}"#, &mut repl);
285 match result {
286 Some(RlmToolResult::Output(s)) => assert_eq!(s, "line 1\nline 2"),
287 _ => panic!("expected Output"),
288 }
289 }
290
291 #[test]
292 fn dispatch_tail() {
293 let ctx = "line 1\nline 2\nline 3\nline 4\nline 5".to_string();
294 let mut repl = RlmRepl::new(ctx, ReplRuntime::Rust);
295 let result = dispatch_tool_call("rlm_tail", r#"{"n": 2}"#, &mut repl);
296 match result {
297 Some(RlmToolResult::Output(s)) => assert_eq!(s, "line 4\nline 5"),
298 _ => panic!("expected Output"),
299 }
300 }
301
302 #[test]
303 fn dispatch_grep() {
304 let ctx = "error: fail\ninfo: ok\nerror: boom".to_string();
305 let mut repl = RlmRepl::new(ctx, ReplRuntime::Rust);
306 let result = dispatch_tool_call("rlm_grep", r#"{"pattern": "error"}"#, &mut repl);
307 match result {
308 Some(RlmToolResult::Output(s)) => {
309 assert!(s.contains("error: fail"));
310 assert!(s.contains("error: boom"));
311 }
312 _ => panic!("expected Output"),
313 }
314 }
315
316 #[test]
317 fn dispatch_final() {
318 let ctx = "whatever".to_string();
319 let mut repl = RlmRepl::new(ctx, ReplRuntime::Rust);
320 let result =
321 dispatch_tool_call("rlm_final", r#"{"answer": "The answer is 42"}"#, &mut repl);
322 match result {
323 Some(RlmToolResult::Final(s)) => assert_eq!(s, "The answer is 42"),
324 _ => panic!("expected Final"),
325 }
326 }
327
328 #[test]
329 fn dispatch_unknown_returns_none() {
330 let ctx = "data".to_string();
331 let mut repl = RlmRepl::new(ctx, ReplRuntime::Rust);
332 assert!(dispatch_tool_call("unknown_tool", "{}", &mut repl).is_none());
333 }
334}