use crate::provider::ToolDefinition;
pub fn rlm_tool_definitions() -> Vec<ToolDefinition> {
vec![
ToolDefinition {
name: "rlm_head".to_string(),
description: "Return the first N lines of the loaded context.".to_string(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"n": {
"type": "integer",
"description": "Number of lines from the start (default: 10)"
}
},
"required": []
}),
},
ToolDefinition {
name: "rlm_tail".to_string(),
description: "Return the last N lines of the loaded context.".to_string(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"n": {
"type": "integer",
"description": "Number of lines from the end (default: 10)"
}
},
"required": []
}),
},
ToolDefinition {
name: "rlm_grep".to_string(),
description: "Search the loaded context for lines matching a regex pattern. Returns matching lines with line numbers.".to_string(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"pattern": {
"type": "string",
"description": "Regex pattern to search for"
}
},
"required": ["pattern"]
}),
},
ToolDefinition {
name: "rlm_count".to_string(),
description: "Count occurrences of a regex pattern in the loaded context.".to_string(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"pattern": {
"type": "string",
"description": "Regex pattern to count"
}
},
"required": ["pattern"]
}),
},
ToolDefinition {
name: "rlm_slice".to_string(),
description: "Return a slice of the context by line range.".to_string(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"start": {
"type": "integer",
"description": "Start line number (0-indexed)"
},
"end": {
"type": "integer",
"description": "End line number (exclusive)"
}
},
"required": ["start", "end"]
}),
},
ToolDefinition {
name: "rlm_llm_query".to_string(),
description: "Ask a focused sub-question about a portion of the context. Use this for semantic understanding of specific sections.".to_string(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The question to answer about the context"
},
"context_slice": {
"type": "string",
"description": "Optional: specific text slice to analyze (if omitted, uses full context)"
}
},
"required": ["query"]
}),
},
ToolDefinition {
name: "rlm_final".to_string(),
description: "Return the final answer to the analysis query. Call this when you have gathered enough information to answer.".to_string(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"answer": {
"type": "string",
"description": "The complete, detailed answer to the original query"
}
},
"required": ["answer"]
}),
},
ToolDefinition {
name: "rlm_ast_query".to_string(),
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(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Tree-sitter S-expression query (e.g., '(function_item name: (identifier) @name)')"
}
},
"required": ["query"]
}),
},
]
}
pub enum RlmToolResult {
Output(String),
Final(String),
}
pub fn dispatch_tool_call(
name: &str,
arguments: &str,
repl: &mut super::repl::RlmRepl,
) -> Option<RlmToolResult> {
let args: serde_json::Value = serde_json::from_str(arguments).unwrap_or_default();
match name {
"rlm_head" => {
let n = args.get("n").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
let output = repl.head(n).join("\n");
Some(RlmToolResult::Output(output))
}
"rlm_tail" => {
let n = args.get("n").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
let output = repl.tail(n).join("\n");
Some(RlmToolResult::Output(output))
}
"rlm_grep" => {
let pattern = args.get("pattern").and_then(|v| v.as_str()).unwrap_or("");
let matches = repl.grep(pattern);
let output = matches
.iter()
.map(|(i, line)| format!("{}:{}", i, line))
.collect::<Vec<_>>()
.join("\n");
if output.is_empty() {
Some(RlmToolResult::Output("(no matches)".to_string()))
} else {
Some(RlmToolResult::Output(output))
}
}
"rlm_count" => {
let pattern = args.get("pattern").and_then(|v| v.as_str()).unwrap_or("");
let count = repl.count(pattern);
Some(RlmToolResult::Output(count.to_string()))
}
"rlm_slice" => {
let start = args.get("start").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
let end = args.get("end").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
let output = repl.slice(start, end).to_string();
Some(RlmToolResult::Output(output))
}
"rlm_llm_query" => {
let query = args
.get("query")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let context_slice = args
.get("context_slice")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let payload = serde_json::json!({
"__rlm_llm_query": true,
"query": query,
"context_slice": context_slice,
});
Some(RlmToolResult::Output(payload.to_string()))
}
"rlm_final" => {
let answer = args
.get("answer")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
Some(RlmToolResult::Final(answer))
}
"rlm_ast_query" => {
let query = args
.get("query")
.and_then(|v| v.as_str())
.unwrap_or("");
let mut oracle = super::oracle::TreeSitterOracle::new(repl.context().to_string());
match oracle.query(query) {
Ok(result) => {
let matches: Vec<serde_json::Value> = result.matches.iter().map(|m| {
serde_json::json!({
"line": m.line,
"column": m.column,
"captures": m.captures,
"text": m.text
})
}).collect();
let output = serde_json::json!({
"query": query,
"match_count": matches.len(),
"matches": matches
});
Some(RlmToolResult::Output(output.to_string()))
}
Err(e) => {
Some(RlmToolResult::Output(format!("AST query error: {}", e)))
}
}
}
_ => None, }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rlm::repl::{ReplRuntime, RlmRepl};
#[test]
fn tool_definitions_are_complete() {
let defs = rlm_tool_definitions();
assert_eq!(defs.len(), 8);
let names: Vec<&str> = defs.iter().map(|d| d.name.as_str()).collect();
assert!(names.contains(&"rlm_head"));
assert!(names.contains(&"rlm_tail"));
assert!(names.contains(&"rlm_grep"));
assert!(names.contains(&"rlm_count"));
assert!(names.contains(&"rlm_slice"));
assert!(names.contains(&"rlm_llm_query"));
assert!(names.contains(&"rlm_final"));
assert!(names.contains(&"rlm_ast_query"));
}
#[test]
fn dispatch_head() {
let ctx = "line 1\nline 2\nline 3\nline 4\nline 5".to_string();
let mut repl = RlmRepl::new(ctx, ReplRuntime::Rust);
let result = dispatch_tool_call("rlm_head", r#"{"n": 2}"#, &mut repl);
match result {
Some(RlmToolResult::Output(s)) => assert_eq!(s, "line 1\nline 2"),
_ => panic!("expected Output"),
}
}
#[test]
fn dispatch_tail() {
let ctx = "line 1\nline 2\nline 3\nline 4\nline 5".to_string();
let mut repl = RlmRepl::new(ctx, ReplRuntime::Rust);
let result = dispatch_tool_call("rlm_tail", r#"{"n": 2}"#, &mut repl);
match result {
Some(RlmToolResult::Output(s)) => assert_eq!(s, "line 4\nline 5"),
_ => panic!("expected Output"),
}
}
#[test]
fn dispatch_grep() {
let ctx = "error: fail\ninfo: ok\nerror: boom".to_string();
let mut repl = RlmRepl::new(ctx, ReplRuntime::Rust);
let result = dispatch_tool_call("rlm_grep", r#"{"pattern": "error"}"#, &mut repl);
match result {
Some(RlmToolResult::Output(s)) => {
assert!(s.contains("error: fail"));
assert!(s.contains("error: boom"));
}
_ => panic!("expected Output"),
}
}
#[test]
fn dispatch_final() {
let ctx = "whatever".to_string();
let mut repl = RlmRepl::new(ctx, ReplRuntime::Rust);
let result =
dispatch_tool_call("rlm_final", r#"{"answer": "The answer is 42"}"#, &mut repl);
match result {
Some(RlmToolResult::Final(s)) => assert_eq!(s, "The answer is 42"),
_ => panic!("expected Final"),
}
}
#[test]
fn dispatch_unknown_returns_none() {
let ctx = "data".to_string();
let mut repl = RlmRepl::new(ctx, ReplRuntime::Rust);
assert!(dispatch_tool_call("unknown_tool", "{}", &mut repl).is_none());
}
}