Skip to main content

mdx_rust_analysis/
finders.rs

1//! Source finders using tree-sitter + syn for deep understanding of Rig agents.
2//!
3//! Extracts high-signal artifacts (preambles, tools, entrypoints) to feed the optimizer LLM.
4
5use std::path::Path;
6use tree_sitter::{Parser, Tree};
7
8#[derive(Debug, Clone)]
9pub struct ExtractedPrompt {
10    pub file: String,
11    pub line: usize,
12    pub text: String,
13}
14
15#[derive(Debug, Clone)]
16pub struct ExtractedTool {
17    pub file: String,
18    pub name: String,
19    pub description: Option<String>,
20}
21
22#[derive(Debug, Clone)]
23pub struct AgentEntrypoint {
24    pub name: String,
25    pub file: String,
26    pub line: usize,
27}
28
29/// Parse Rust source into a tree-sitter tree.
30pub fn parse_rust_source(source: &str) -> Option<Tree> {
31    let mut parser = Parser::new();
32    parser
33        .set_language(&tree_sitter_rust::LANGUAGE.into())
34        .ok()?;
35    parser.parse(source, None)
36}
37
38/// Find functions that look like agent entrypoints (run_agent, run, main agent fn).
39pub fn find_run_agent_functions(source: &str) -> Vec<AgentEntrypoint> {
40    let Some(tree) = parse_rust_source(source) else {
41        return vec![];
42    };
43
44    let mut entries = vec![];
45    let root = tree.root_node();
46
47    for node in root.children(&mut root.walk()) {
48        if node.kind() == "function_item" {
49            if let Some(name_node) = node.child_by_field_name("name") {
50                let name = name_node.utf8_text(source.as_bytes()).unwrap_or("");
51                if name.contains("run_agent") || name == "run" || name.contains("agent") {
52                    let line = node.start_position().row + 1;
53                    entries.push(AgentEntrypoint {
54                        name: name.to_string(),
55                        file: "<unknown>".to_string(),
56                        line,
57                    });
58                }
59            }
60        }
61    }
62    entries
63}
64
65/// Extract preamble strings from Rig-style `.preamble("...")` calls.
66/// Uses simple but effective tree-sitter + string heuristics for now.
67pub fn find_preambles(source: &str, file_path: &Path) -> Vec<ExtractedPrompt> {
68    let mut prompts = vec![];
69
70    // Tree-sitter walk for call expressions containing "preamble"
71    if let Some(tree) = parse_rust_source(source) {
72        let root = tree.root_node();
73        let _cursor = root.walk();
74
75        fn walk(
76            node: tree_sitter::Node,
77            source: &str,
78            file_path: &Path,
79            prompts: &mut Vec<ExtractedPrompt>,
80        ) {
81            if node.kind() == "call_expression" {
82                let text = node.utf8_text(source.as_bytes()).unwrap_or("");
83                if text.contains("preamble(") {
84                    // Try to extract the string literal argument
85                    if let Some(start) = text.find("preamble(\"") {
86                        let after = &text[start + 10..];
87                        if let Some(end) = after.find('"') {
88                            let content = &after[..end];
89                            if !content.is_empty() && content.len() > 3 {
90                                let line = node.start_position().row + 1;
91                                prompts.push(ExtractedPrompt {
92                                    file: file_path.display().to_string(),
93                                    line,
94                                    text: content.to_string(),
95                                });
96                            }
97                        }
98                    }
99                }
100            }
101            let mut c = node.walk();
102            for child in node.children(&mut c) {
103                walk(child, source, file_path, prompts);
104            }
105        }
106
107        walk(root, source, file_path, &mut prompts);
108    }
109
110    // Fallback: regex-style scan for any .preamble("...")
111    if prompts.is_empty() {
112        for line in source.lines() {
113            if line.contains(".preamble(\"") {
114                if let Some(start) = line.find(".preamble(\"") {
115                    let after = &line[start + 11..];
116                    if let Some(end) = after.find('"') {
117                        let content = &after[..end];
118                        if !content.is_empty() {
119                            prompts.push(ExtractedPrompt {
120                                file: file_path.display().to_string(),
121                                line: prompts.len() + 1, // approximate line, good enough for fallback
122                                text: content.to_string(),
123                            });
124                        }
125                    }
126                }
127            }
128        }
129    }
130
131    prompts
132}
133
134/// Very rough tool extraction (looks for .tool( or tool definitions).
135pub fn find_tools(source: &str, file_path: &Path) -> Vec<ExtractedTool> {
136    let mut tools = vec![];
137
138    for line in source.lines() {
139        if line.contains(".tool(") || line.contains("tool(") {
140            // Try to grab a name near the call
141            let name = if let Some(start) = line.find(".tool(") {
142                let s = &line[start + 6..];
143                s.split(|c: char| !c.is_alphanumeric() && c != '_')
144                    .find(|p| !p.is_empty())
145                    .unwrap_or("tool")
146                    .to_string()
147            } else {
148                "tool".to_string()
149            };
150
151            tools.push(ExtractedTool {
152                file: file_path.display().to_string(),
153                name,
154                description: None,
155            });
156        }
157    }
158
159    tools
160}
161
162/// Heuristic: does this source contain a Rig agent?
163pub fn looks_like_rig_agent(source: &str) -> bool {
164    source.contains("rig::") || source.contains(".agent(") || source.contains("preamble(")
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn test_find_preambles_basic() {
173        let source = r#"
174            let agent = client.agent("gpt-4o").preamble("You are helpful. Think step by step.").build();
175        "#;
176        let prompts = find_preambles(source, std::path::Path::new("test.rs"));
177        assert!(!prompts.is_empty());
178        assert!(prompts[0].text.contains("Think step by step"));
179    }
180
181    #[test]
182    fn test_looks_like_rig_agent() {
183        assert!(looks_like_rig_agent(
184            "let x = client.agent(\"..\").preamble(\"hi\")"
185        ));
186        assert!(!looks_like_rig_agent("fn main() {}"));
187    }
188
189    #[test]
190    fn test_find_run_agent_functions() {
191        let source = "pub async fn run_agent(input: Input) -> Result<Output> { Ok(()) }";
192        let fns = find_run_agent_functions(source);
193        assert!(!fns.is_empty());
194    }
195}