Skip to main content

codetether_agent/agent/
builtin.rs

1//! Built-in agent definitions
2
3use super::{AgentInfo, AgentMode};
4use std::path::Path;
5
6/// The default "build" agent - full access for development work
7pub fn build_agent() -> AgentInfo {
8    AgentInfo {
9        name: "build".to_string(),
10        description: Some("Full access agent for development work".to_string()),
11        mode: AgentMode::Primary,
12        native: true,
13        hidden: false,
14        model: None,
15        temperature: None,
16        top_p: None,
17        max_steps: Some(100),
18    }
19}
20
21/// The "plan" agent - read-only for analysis and exploration
22pub fn plan_agent() -> AgentInfo {
23    AgentInfo {
24        name: "plan".to_string(),
25        description: Some("Read-only agent for analysis and code exploration".to_string()),
26        mode: AgentMode::Primary,
27        native: true,
28        hidden: false,
29        model: None,
30        temperature: None,
31        top_p: None,
32        max_steps: Some(50),
33    }
34}
35
36/// The "explore" agent - fast codebase exploration
37pub fn explore_agent() -> AgentInfo {
38    AgentInfo {
39        name: "explore".to_string(),
40        description: Some("Fast agent for exploring codebases".to_string()),
41        mode: AgentMode::Subagent,
42        native: true,
43        hidden: false,
44        model: None,
45        temperature: None,
46        top_p: None,
47        max_steps: Some(20),
48    }
49}
50
51/// System prompt for the build agent
52#[allow(dead_code)]
53pub const BUILD_SYSTEM_PROMPT: &str = r#"You are an expert AI programming assistant called CodeTether Agent.
54
55You help users with software development tasks including:
56- Writing and editing code
57- Debugging and fixing issues
58- Implementing new features
59- Refactoring and improving code quality
60- Explaining code and concepts
61
62You have access to tools that let you:
63- Read and write files
64- Run shell commands
65- Search the codebase
66- List directories
67
68Always:
69- Be concise and helpful
70- Show your work by using tools
71- Explain what you're doing
72- Ask clarifying questions when needed
73- Follow best practices for the language/framework being used
74
75Current working directory: {cwd}
76"#;
77
78/// System prompt for the plan agent
79#[allow(dead_code)]
80pub const PLAN_SYSTEM_PROMPT: &str = r#"You are an expert AI assistant for code analysis and planning.
81
82Your role is to:
83- Explore and understand codebases
84- Analyze code structure and architecture
85- Plan changes and refactoring
86- Answer questions about the code
87
88You have read-only access to the codebase. You can:
89- Read files
90- Search the codebase
91- List directories
92- Run safe commands
93
94You should NOT modify any files. If the user wants changes, explain what should be changed and suggest switching to the build agent.
95
96Current working directory: {cwd}
97"#;
98
99/// System prompt for the explore agent
100///
101/// This constant is available for use when creating an explore agent programmatically.
102/// Currently used via the `explore_agent()` function which returns the AgentInfo.
103#[allow(dead_code)]
104pub const EXPLORE_SYSTEM_PROMPT: &str = r#"You are a fast, focused agent for codebase exploration.
105
106Your job is to quickly find relevant code and information. Use tools efficiently:
107- Use glob to find files by pattern
108- Use grep to search for text
109- Use list to see directory contents
110- Read files to get details
111
112Be thorough but fast. Return the most relevant results without unnecessary exploration.
113"#;
114
115/// Load AGENTS.md from the given directory or any parent directory.
116/// Returns the content and path if found.
117pub fn load_agents_md(start_dir: &Path) -> Option<(String, std::path::PathBuf)> {
118    let mut current = start_dir.to_path_buf();
119    let repo_root = find_git_root(start_dir);
120
121    loop {
122        let agents_path = current.join("AGENTS.md");
123        if agents_path.exists() {
124            if let Ok(content) = std::fs::read_to_string(&agents_path) {
125                return Some((content, agents_path));
126            }
127        }
128
129        // Stop traversal at repository boundary.
130        if repo_root.as_ref() == Some(&current) {
131            break;
132        }
133
134        // Try parent directory
135        if !current.pop() {
136            break;
137        }
138    }
139
140    None
141}
142
143/// Load all AGENTS.md files from the given directory up to the root.
144/// Returns a list of (content, path) tuples, from most specific (closest to start_dir) to least.
145/// Traversal stops at the nearest git repository root (directory containing `.git`).
146pub fn load_all_agents_md(start_dir: &Path) -> Vec<(String, std::path::PathBuf)> {
147    let mut results = Vec::new();
148    let mut current = start_dir.to_path_buf();
149    let repo_root = find_git_root(start_dir);
150
151    loop {
152        let agents_path = current.join("AGENTS.md");
153        if agents_path.exists() {
154            if let Ok(content) = std::fs::read_to_string(&agents_path) {
155                results.push((content, agents_path));
156            }
157        }
158
159        // Stop traversal at repository boundary.
160        if repo_root.as_ref() == Some(&current) {
161            break;
162        }
163
164        // Try parent directory
165        if !current.pop() {
166            break;
167        }
168    }
169
170    results
171}
172
173fn find_git_root(start_dir: &Path) -> Option<std::path::PathBuf> {
174    let mut current = start_dir.to_path_buf();
175
176    loop {
177        if current.join(".git").exists() {
178            return Some(current);
179        }
180
181        if !current.pop() {
182            break;
183        }
184    }
185
186    None
187}
188
189/// Build a complete system prompt for the build agent, including AGENTS.md content if present.
190pub fn build_system_prompt(cwd: &Path) -> String {
191    let base_prompt = BUILD_SYSTEM_PROMPT.replace("{cwd}", &cwd.display().to_string());
192
193    // Load AGENTS.md files (closest first)
194    let agents_files = load_all_agents_md(cwd);
195
196    if agents_files.is_empty() {
197        return base_prompt;
198    }
199
200    // Build the AGENTS.md section - include all found files, closest last (takes precedence)
201    let mut agents_section = String::new();
202    agents_section.push_str("\n\n## Project Instructions (AGENTS.md)\n\n");
203    agents_section
204        .push_str("The following instructions were loaded from AGENTS.md files in the project.\n");
205    agents_section
206        .push_str("Follow these project-specific guidelines when working on this codebase.\n\n");
207
208    // Reverse so closest (most specific) comes last and takes precedence
209    for (content, path) in agents_files.iter().rev() {
210        agents_section.push_str(&format!("### From {}\n\n", path.display()));
211        agents_section.push_str(content);
212        agents_section.push_str("\n\n");
213    }
214
215    format!("{base_prompt}{agents_section}")
216}
217
218/// Build a complete system prompt for the plan agent, including AGENTS.md content if present.
219#[allow(dead_code)]
220pub fn build_plan_system_prompt(cwd: &Path) -> String {
221    let base_prompt = PLAN_SYSTEM_PROMPT.replace("{cwd}", &cwd.display().to_string());
222
223    // Load AGENTS.md files
224    let agents_files = load_all_agents_md(cwd);
225
226    if agents_files.is_empty() {
227        return base_prompt;
228    }
229
230    let mut agents_section = String::new();
231    agents_section.push_str("\n\n## Project Instructions (AGENTS.md)\n\n");
232
233    for (content, path) in agents_files.iter().rev() {
234        agents_section.push_str(&format!("### From {}\n\n", path.display()));
235        agents_section.push_str(content);
236        agents_section.push_str("\n\n");
237    }
238
239    format!("{base_prompt}{agents_section}")
240}
241
242#[cfg(test)]
243mod tests {
244    use super::{load_agents_md, load_all_agents_md};
245    use tempfile::tempdir;
246
247    #[test]
248    fn load_agents_md_stops_at_git_root() {
249        let tmp = tempdir().expect("tempdir");
250        let parent_agents = tmp.path().join("AGENTS.md");
251        std::fs::write(&parent_agents, "parent instructions").expect("write parent AGENTS");
252
253        let repo_root = tmp.path().join("repo");
254        let nested = repo_root.join("src/nested");
255        std::fs::create_dir_all(repo_root.join(".git")).expect("create .git dir");
256        std::fs::create_dir_all(&nested).expect("create nested dir");
257        let repo_agents = repo_root.join("AGENTS.md");
258        std::fs::write(&repo_agents, "repo instructions").expect("write repo AGENTS");
259
260        let loaded = load_agents_md(&nested).expect("expected AGENTS");
261        assert_eq!(loaded.1, repo_agents);
262        assert_eq!(loaded.0, "repo instructions");
263    }
264
265    #[test]
266    fn load_all_agents_md_collects_within_repo_only() {
267        let tmp = tempdir().expect("tempdir");
268        let parent_agents = tmp.path().join("AGENTS.md");
269        std::fs::write(&parent_agents, "parent instructions").expect("write parent AGENTS");
270
271        let repo_root = tmp.path().join("repo");
272        let subdir = repo_root.join("service");
273        let nested = subdir.join("src");
274        std::fs::create_dir_all(repo_root.join(".git")).expect("create .git dir");
275        std::fs::create_dir_all(&nested).expect("create nested dir");
276        let repo_agents = repo_root.join("AGENTS.md");
277        std::fs::write(&repo_agents, "repo instructions").expect("write repo AGENTS");
278        let sub_agents = subdir.join("AGENTS.md");
279        std::fs::write(&sub_agents, "sub instructions").expect("write sub AGENTS");
280
281        let loaded = load_all_agents_md(&nested);
282
283        assert_eq!(loaded.len(), 2);
284        assert_eq!(loaded[0].1, sub_agents);
285        assert_eq!(loaded[1].1, repo_agents);
286        assert!(!loaded.iter().any(|(_, path)| path == &parent_agents));
287    }
288}