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- Spawn specialized sub-agents and delegate tasks to them
68
69For complex tasks, use the `agent` tool to spawn focused sub-agents:
70- Spawn a sub-agent with a specific role (e.g., "reviewer", "architect", "tester")
71- Send it targeted messages and get back results
72- Each sub-agent has its own conversation history and full tool access
73- Use sub-agents when a task benefits from a dedicated focus or parallel exploration
74
75Always:
76- Be concise and helpful
77- Show your work by using tools
78- Explain what you're doing
79- Ask clarifying questions when needed
80- Follow best practices for the language/framework being used
81
82Current working directory: {cwd}
83"#;
84
85/// System prompt for the plan agent
86#[allow(dead_code)]
87pub const PLAN_SYSTEM_PROMPT: &str = r#"You are an expert AI assistant for code analysis and planning.
88
89Your role is to:
90- Explore and understand codebases
91- Analyze code structure and architecture
92- Plan changes and refactoring
93- Answer questions about the code
94
95You have read-only access to the codebase. You can:
96- Read files
97- Search the codebase
98- List directories
99- Run safe commands
100
101You should NOT modify any files. If the user wants changes, explain what should be changed and suggest switching to the build agent.
102
103Current working directory: {cwd}
104"#;
105
106/// System prompt for the explore agent
107///
108/// This constant is available for use when creating an explore agent programmatically.
109/// Currently used via the `explore_agent()` function which returns the AgentInfo.
110#[allow(dead_code)]
111pub const EXPLORE_SYSTEM_PROMPT: &str = r#"You are a fast, focused agent for codebase exploration.
112
113Your job is to quickly find relevant code and information. Use tools efficiently:
114- Use glob to find files by pattern
115- Use grep to search for text
116- Use list to see directory contents
117- Read files to get details
118
119Be thorough but fast. Return the most relevant results without unnecessary exploration.
120"#;
121
122/// Load AGENTS.md from the given directory or any parent directory.
123/// Returns the content and path if found.
124pub fn load_agents_md(start_dir: &Path) -> Option<(String, std::path::PathBuf)> {
125    let mut current = start_dir.to_path_buf();
126    let repo_root = find_git_root(start_dir);
127
128    loop {
129        let agents_path = current.join("AGENTS.md");
130        if agents_path.exists() {
131            if let Ok(content) = std::fs::read_to_string(&agents_path) {
132                return Some((content, agents_path));
133            }
134        }
135
136        // Stop traversal at repository boundary.
137        if repo_root.as_ref() == Some(&current) {
138            break;
139        }
140
141        // Try parent directory
142        if !current.pop() {
143            break;
144        }
145    }
146
147    None
148}
149
150/// Load all AGENTS.md files from the given directory up to the root.
151/// Returns a list of (content, path) tuples, from most specific (closest to start_dir) to least.
152/// Traversal stops at the nearest git repository root (directory containing `.git`).
153pub fn load_all_agents_md(start_dir: &Path) -> Vec<(String, std::path::PathBuf)> {
154    let mut results = Vec::new();
155    let mut current = start_dir.to_path_buf();
156    let repo_root = find_git_root(start_dir);
157
158    loop {
159        let agents_path = current.join("AGENTS.md");
160        if agents_path.exists() {
161            if let Ok(content) = std::fs::read_to_string(&agents_path) {
162                results.push((content, agents_path));
163            }
164        }
165
166        // Stop traversal at repository boundary.
167        if repo_root.as_ref() == Some(&current) {
168            break;
169        }
170
171        // Try parent directory
172        if !current.pop() {
173            break;
174        }
175    }
176
177    results
178}
179
180fn find_git_root(start_dir: &Path) -> Option<std::path::PathBuf> {
181    let mut current = start_dir.to_path_buf();
182
183    loop {
184        if current.join(".git").exists() {
185            return Some(current);
186        }
187
188        if !current.pop() {
189            break;
190        }
191    }
192
193    None
194}
195
196/// Build a complete system prompt for the build agent, including AGENTS.md content if present.
197pub fn build_system_prompt(cwd: &Path) -> String {
198    let base_prompt = BUILD_SYSTEM_PROMPT.replace("{cwd}", &cwd.display().to_string());
199
200    // Load AGENTS.md files (closest first)
201    let agents_files = load_all_agents_md(cwd);
202
203    if agents_files.is_empty() {
204        return base_prompt;
205    }
206
207    // Build the AGENTS.md section - include all found files, closest last (takes precedence)
208    let mut agents_section = String::new();
209    agents_section.push_str("\n\n## Project Instructions (AGENTS.md)\n\n");
210    agents_section
211        .push_str("The following instructions were loaded from AGENTS.md files in the project.\n");
212    agents_section
213        .push_str("Follow these project-specific guidelines when working on this codebase.\n\n");
214
215    // Reverse so closest (most specific) comes last and takes precedence
216    for (content, path) in agents_files.iter().rev() {
217        agents_section.push_str(&format!("### From {}\n\n", path.display()));
218        agents_section.push_str(content);
219        agents_section.push_str("\n\n");
220    }
221
222    format!("{base_prompt}{agents_section}")
223}
224
225/// Build a complete system prompt for the plan agent, including AGENTS.md content if present.
226#[allow(dead_code)]
227pub fn build_plan_system_prompt(cwd: &Path) -> String {
228    let base_prompt = PLAN_SYSTEM_PROMPT.replace("{cwd}", &cwd.display().to_string());
229
230    // Load AGENTS.md files
231    let agents_files = load_all_agents_md(cwd);
232
233    if agents_files.is_empty() {
234        return base_prompt;
235    }
236
237    let mut agents_section = String::new();
238    agents_section.push_str("\n\n## Project Instructions (AGENTS.md)\n\n");
239
240    for (content, path) in agents_files.iter().rev() {
241        agents_section.push_str(&format!("### From {}\n\n", path.display()));
242        agents_section.push_str(content);
243        agents_section.push_str("\n\n");
244    }
245
246    format!("{base_prompt}{agents_section}")
247}
248
249#[cfg(test)]
250mod tests {
251    use super::{load_agents_md, load_all_agents_md};
252    use tempfile::tempdir;
253
254    #[test]
255    fn load_agents_md_stops_at_git_root() {
256        let tmp = tempdir().expect("tempdir");
257        let parent_agents = tmp.path().join("AGENTS.md");
258        std::fs::write(&parent_agents, "parent instructions").expect("write parent AGENTS");
259
260        let repo_root = tmp.path().join("repo");
261        let nested = repo_root.join("src/nested");
262        std::fs::create_dir_all(repo_root.join(".git")).expect("create .git dir");
263        std::fs::create_dir_all(&nested).expect("create nested dir");
264        let repo_agents = repo_root.join("AGENTS.md");
265        std::fs::write(&repo_agents, "repo instructions").expect("write repo AGENTS");
266
267        let loaded = load_agents_md(&nested).expect("expected AGENTS");
268        assert_eq!(loaded.1, repo_agents);
269        assert_eq!(loaded.0, "repo instructions");
270    }
271
272    #[test]
273    fn load_all_agents_md_collects_within_repo_only() {
274        let tmp = tempdir().expect("tempdir");
275        let parent_agents = tmp.path().join("AGENTS.md");
276        std::fs::write(&parent_agents, "parent instructions").expect("write parent AGENTS");
277
278        let repo_root = tmp.path().join("repo");
279        let subdir = repo_root.join("service");
280        let nested = subdir.join("src");
281        std::fs::create_dir_all(repo_root.join(".git")).expect("create .git dir");
282        std::fs::create_dir_all(&nested).expect("create nested dir");
283        let repo_agents = repo_root.join("AGENTS.md");
284        std::fs::write(&repo_agents, "repo instructions").expect("write repo AGENTS");
285        let sub_agents = subdir.join("AGENTS.md");
286        std::fs::write(&sub_agents, "sub instructions").expect("write sub AGENTS");
287
288        let loaded = load_all_agents_md(&nested);
289
290        assert_eq!(loaded.len(), 2);
291        assert_eq!(loaded[0].1, sub_agents);
292        assert_eq!(loaded[1].1, repo_agents);
293        assert!(!loaded.iter().any(|(_, path)| path == &parent_agents));
294    }
295}