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