An AI agent built to do Ralph loops - plan mode for planning and ralph mode for implementing.

feat: implement planning agent with interactive conversation

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

+427 -4
+135
Cargo.lock
··· 187 187 checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 188 188 189 189 [[package]] 190 + name = "dirs" 191 + version = "5.0.1" 192 + source = "registry+https://github.com/rust-lang/crates.io-index" 193 + checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" 194 + dependencies = [ 195 + "dirs-sys", 196 + ] 197 + 198 + [[package]] 199 + name = "dirs-sys" 200 + version = "0.4.1" 201 + source = "registry+https://github.com/rust-lang/crates.io-index" 202 + checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 203 + dependencies = [ 204 + "libc", 205 + "option-ext", 206 + "redox_users", 207 + "windows-sys 0.48.0", 208 + ] 209 + 210 + [[package]] 190 211 name = "displaydoc" 191 212 version = "0.2.5" 192 213 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 681 702 checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" 682 703 683 704 [[package]] 705 + name = "libredox" 706 + version = "0.1.12" 707 + source = "registry+https://github.com/rust-lang/crates.io-index" 708 + checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" 709 + dependencies = [ 710 + "bitflags", 711 + "libc", 712 + ] 713 + 714 + [[package]] 684 715 name = "linux-raw-sys" 685 716 version = "0.11.0" 686 717 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 804 835 ] 805 836 806 837 [[package]] 838 + name = "option-ext" 839 + version = "0.2.0" 840 + source = "registry+https://github.com/rust-lang/crates.io-index" 841 + checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 842 + 843 + [[package]] 807 844 name = "parking_lot" 808 845 version = "0.12.5" 809 846 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 908 945 ] 909 946 910 947 [[package]] 948 + name = "redox_users" 949 + version = "0.4.6" 950 + source = "registry+https://github.com/rust-lang/crates.io-index" 951 + checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" 952 + dependencies = [ 953 + "getrandom 0.2.17", 954 + "libredox", 955 + "thiserror", 956 + ] 957 + 958 + [[package]] 911 959 name = "regex" 912 960 version = "1.12.2" 913 961 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 997 1045 "anyhow", 998 1046 "async-trait", 999 1047 "clap", 1048 + "dirs", 1000 1049 "env_logger", 1001 1050 "log", 1002 1051 "reqwest", ··· 1289 1338 ] 1290 1339 1291 1340 [[package]] 1341 + name = "thiserror" 1342 + version = "1.0.69" 1343 + source = "registry+https://github.com/rust-lang/crates.io-index" 1344 + checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1345 + dependencies = [ 1346 + "thiserror-impl", 1347 + ] 1348 + 1349 + [[package]] 1350 + name = "thiserror-impl" 1351 + version = "1.0.69" 1352 + source = "registry+https://github.com/rust-lang/crates.io-index" 1353 + checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1354 + dependencies = [ 1355 + "proc-macro2", 1356 + "quote", 1357 + "syn", 1358 + ] 1359 + 1360 + [[package]] 1292 1361 name = "tinystr" 1293 1362 version = "0.8.2" 1294 1363 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1642 1711 1643 1712 [[package]] 1644 1713 name = "windows-sys" 1714 + version = "0.48.0" 1715 + source = "registry+https://github.com/rust-lang/crates.io-index" 1716 + checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1717 + dependencies = [ 1718 + "windows-targets 0.48.5", 1719 + ] 1720 + 1721 + [[package]] 1722 + name = "windows-sys" 1645 1723 version = "0.52.0" 1646 1724 source = "registry+https://github.com/rust-lang/crates.io-index" 1647 1725 checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" ··· 1669 1747 1670 1748 [[package]] 1671 1749 name = "windows-targets" 1750 + version = "0.48.5" 1751 + source = "registry+https://github.com/rust-lang/crates.io-index" 1752 + checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1753 + dependencies = [ 1754 + "windows_aarch64_gnullvm 0.48.5", 1755 + "windows_aarch64_msvc 0.48.5", 1756 + "windows_i686_gnu 0.48.5", 1757 + "windows_i686_msvc 0.48.5", 1758 + "windows_x86_64_gnu 0.48.5", 1759 + "windows_x86_64_gnullvm 0.48.5", 1760 + "windows_x86_64_msvc 0.48.5", 1761 + ] 1762 + 1763 + [[package]] 1764 + name = "windows-targets" 1672 1765 version = "0.52.6" 1673 1766 source = "registry+https://github.com/rust-lang/crates.io-index" 1674 1767 checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" ··· 1702 1795 1703 1796 [[package]] 1704 1797 name = "windows_aarch64_gnullvm" 1798 + version = "0.48.5" 1799 + source = "registry+https://github.com/rust-lang/crates.io-index" 1800 + checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1801 + 1802 + [[package]] 1803 + name = "windows_aarch64_gnullvm" 1705 1804 version = "0.52.6" 1706 1805 source = "registry+https://github.com/rust-lang/crates.io-index" 1707 1806 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" ··· 1714 1813 1715 1814 [[package]] 1716 1815 name = "windows_aarch64_msvc" 1816 + version = "0.48.5" 1817 + source = "registry+https://github.com/rust-lang/crates.io-index" 1818 + checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1819 + 1820 + [[package]] 1821 + name = "windows_aarch64_msvc" 1717 1822 version = "0.52.6" 1718 1823 source = "registry+https://github.com/rust-lang/crates.io-index" 1719 1824 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" ··· 1723 1828 version = "0.53.1" 1724 1829 source = "registry+https://github.com/rust-lang/crates.io-index" 1725 1830 checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" 1831 + 1832 + [[package]] 1833 + name = "windows_i686_gnu" 1834 + version = "0.48.5" 1835 + source = "registry+https://github.com/rust-lang/crates.io-index" 1836 + checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1726 1837 1727 1838 [[package]] 1728 1839 name = "windows_i686_gnu" ··· 1750 1861 1751 1862 [[package]] 1752 1863 name = "windows_i686_msvc" 1864 + version = "0.48.5" 1865 + source = "registry+https://github.com/rust-lang/crates.io-index" 1866 + checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1867 + 1868 + [[package]] 1869 + name = "windows_i686_msvc" 1753 1870 version = "0.52.6" 1754 1871 source = "registry+https://github.com/rust-lang/crates.io-index" 1755 1872 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" ··· 1762 1879 1763 1880 [[package]] 1764 1881 name = "windows_x86_64_gnu" 1882 + version = "0.48.5" 1883 + source = "registry+https://github.com/rust-lang/crates.io-index" 1884 + checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1885 + 1886 + [[package]] 1887 + name = "windows_x86_64_gnu" 1765 1888 version = "0.52.6" 1766 1889 source = "registry+https://github.com/rust-lang/crates.io-index" 1767 1890 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" ··· 1774 1897 1775 1898 [[package]] 1776 1899 name = "windows_x86_64_gnullvm" 1900 + version = "0.48.5" 1901 + source = "registry+https://github.com/rust-lang/crates.io-index" 1902 + checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1903 + 1904 + [[package]] 1905 + name = "windows_x86_64_gnullvm" 1777 1906 version = "0.52.6" 1778 1907 source = "registry+https://github.com/rust-lang/crates.io-index" 1779 1908 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" ··· 1783 1912 version = "0.53.1" 1784 1913 source = "registry+https://github.com/rust-lang/crates.io-index" 1785 1914 checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" 1915 + 1916 + [[package]] 1917 + name = "windows_x86_64_msvc" 1918 + version = "0.48.5" 1919 + source = "registry+https://github.com/rust-lang/crates.io-index" 1920 + checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1786 1921 1787 1922 [[package]] 1788 1923 name = "windows_x86_64_msvc"
+1
Cargo.toml
··· 14 14 clap = { version = "4.5", features = ["derive"] } 15 15 log = "0.4" 16 16 env_logger = "0.11" 17 + dirs = "5.0" 17 18 18 19 [dev-dependencies] 19 20 tempfile = "3.15"
+2 -1
src/lib.rs
··· 1 1 pub mod config; 2 2 pub mod llm; 3 + pub mod planning; 4 + pub mod spec; 3 5 pub mod tools; 4 - pub mod spec;
+41 -3
src/main.rs
··· 1 1 pub mod config; 2 2 pub mod llm; 3 + pub mod planning; 3 4 pub mod spec; 4 5 pub mod tools; 5 6 6 7 use clap::{Parser, Subcommand}; 8 + use std::path::PathBuf; 7 9 8 10 #[derive(Parser)] 9 11 #[command(name = "rustagent")] ··· 37 39 }, 38 40 } 39 41 42 + /// Find config file in standard locations 43 + fn find_config_path() -> anyhow::Result<PathBuf> { 44 + // Try current directory 45 + let local_config = PathBuf::from("rustagent.toml"); 46 + if local_config.exists() { 47 + return Ok(local_config); 48 + } 49 + 50 + // Try XDG config directory 51 + if let Some(config_dir) = dirs::config_dir() { 52 + let xdg_config = config_dir.join("rustagent").join("config.toml"); 53 + if xdg_config.exists() { 54 + return Ok(xdg_config); 55 + } 56 + } 57 + 58 + // Try home directory 59 + if let Some(home_dir) = dirs::home_dir() { 60 + let home_config = home_dir.join(".rustagent").join("config.toml"); 61 + if home_config.exists() { 62 + return Ok(home_config); 63 + } 64 + } 65 + 66 + anyhow::bail!("Config file not found. Please create rustagent.toml in current directory or ~/.rustagent/config.toml") 67 + } 68 + 40 69 #[tokio::main] 41 70 async fn main() -> anyhow::Result<()> { 42 71 env_logger::init(); ··· 50 79 // TODO: Implement initialization logic 51 80 } 52 81 Commands::Plan { spec_dir } => { 53 - let dir = spec_dir.clone().unwrap_or_else(|| ".".to_string()); 54 - println!("Creating plan from spec directory: {}", dir); 55 - // TODO: Implement plan creation logic 82 + // Load config from standard locations 83 + let config_path = find_config_path()?; 84 + let config = config::Config::load(&config_path)?; 85 + 86 + // Use provided spec_dir or default from config 87 + let dir = spec_dir 88 + .clone() 89 + .unwrap_or_else(|| config.rustagent.spec_dir.clone()); 90 + 91 + // Create and run planning agent 92 + let mut agent = planning::PlanningAgent::new(config, dir); 93 + agent.run().await?; 56 94 } 57 95 Commands::Run { spec_file, max_iterations } => { 58 96 println!("Running agent with spec: {}", spec_file);
+221
src/planning/mod.rs
··· 1 + use crate::config::{Config, LlmProvider}; 2 + use crate::llm::{LlmClient, Message, ResponseContent, Role}; 3 + use crate::llm::anthropic::AnthropicClient; 4 + use crate::tools::{ToolRegistry, file::{ReadFileTool, WriteFileTool, ListFilesTool}, shell::RunCommandTool}; 5 + use std::io::{self, Write}; 6 + 7 + /// Planning agent for interactive spec creation 8 + pub struct PlanningAgent { 9 + client: Box<dyn LlmClient>, 10 + registry: ToolRegistry, 11 + pub spec_dir: String, 12 + conversation: Vec<Message>, 13 + } 14 + 15 + impl PlanningAgent { 16 + /// Create a new planning agent with the given config 17 + pub fn new(config: Config, spec_dir: String) -> Self { 18 + // Create LLM client based on provider 19 + let client: Box<dyn LlmClient> = match config.llm.provider { 20 + LlmProvider::Anthropic => { 21 + let api_key = config 22 + .anthropic 23 + .expect("Anthropic config required for Anthropic provider") 24 + .api_key; 25 + Box::new(AnthropicClient::new(api_key)) 26 + } 27 + _ => panic!("Unsupported LLM provider"), 28 + }; 29 + 30 + // Create and populate tool registry 31 + let mut registry = ToolRegistry::new(); 32 + registry.register(Box::new(ReadFileTool)); 33 + registry.register(Box::new(WriteFileTool)); 34 + registry.register(Box::new(ListFilesTool)); 35 + registry.register(Box::new(RunCommandTool)); 36 + 37 + // Initialize conversation with system message 38 + let system_message = Message { 39 + role: Role::System, 40 + content: PLANNING_SYSTEM_PROMPT.to_string(), 41 + }; 42 + 43 + Self { 44 + client, 45 + registry, 46 + spec_dir, 47 + conversation: vec![system_message], 48 + } 49 + } 50 + 51 + /// Run the interactive planning loop 52 + pub async fn run(&mut self) -> anyhow::Result<()> { 53 + println!("Planning Agent - Interactive Mode"); 54 + println!("Type your requirements and I'll help you create a detailed spec."); 55 + println!("Type 'done' when finished or 'exit' to quit.\n"); 56 + 57 + loop { 58 + // Get user input 59 + print!("You: "); 60 + io::stdout().flush()?; 61 + 62 + let mut input = String::new(); 63 + io::stdin().read_line(&mut input)?; 64 + let input = input.trim(); 65 + 66 + if input == "exit" { 67 + println!("Exiting planning mode."); 68 + break; 69 + } 70 + 71 + if input == "done" { 72 + println!("Planning complete. Spec saved to {}", self.spec_dir); 73 + break; 74 + } 75 + 76 + if input.is_empty() { 77 + continue; 78 + } 79 + 80 + // Add user message to conversation 81 + self.conversation.push(Message { 82 + role: Role::User, 83 + content: input.to_string(), 84 + }); 85 + 86 + // Process the conversation turn 87 + match self.process_turn().await { 88 + Ok(should_continue) => { 89 + if !should_continue { 90 + break; 91 + } 92 + } 93 + Err(e) => { 94 + eprintln!("Error: {}", e); 95 + // Remove the failed user message 96 + self.conversation.pop(); 97 + } 98 + } 99 + } 100 + 101 + Ok(()) 102 + } 103 + 104 + /// Process a single conversation turn 105 + async fn process_turn(&mut self) -> anyhow::Result<bool> { 106 + loop { 107 + // Get tool definitions 108 + let tools = self.registry.definitions(); 109 + 110 + // Call LLM 111 + let response = self 112 + .client 113 + .chat(self.conversation.clone(), &tools) 114 + .await 115 + .map_err(|e| anyhow::anyhow!("LLM error: {}", e))?; 116 + 117 + match response.content { 118 + ResponseContent::Text(text) => { 119 + // Add assistant response to conversation 120 + self.conversation.push(Message { 121 + role: Role::Assistant, 122 + content: text.clone(), 123 + }); 124 + 125 + // Print assistant response 126 + println!("\nAssistant: {}\n", text); 127 + 128 + // Check for completion 129 + if let Some(stop_reason) = &response.stop_reason { 130 + if stop_reason == "end_turn" { 131 + return Ok(true); 132 + } 133 + } 134 + 135 + return Ok(true); 136 + } 137 + ResponseContent::ToolCalls(tool_calls) => { 138 + // Execute each tool call 139 + for tool_call in &tool_calls { 140 + println!("\n[Executing tool: {}]", tool_call.name); 141 + 142 + // Get the tool from registry 143 + let tool = self 144 + .registry 145 + .get(&tool_call.name) 146 + .ok_or_else(|| anyhow::anyhow!("Tool not found: {}", tool_call.name))?; 147 + 148 + // Execute the tool 149 + let result = tool.execute(tool_call.parameters.clone()).await; 150 + 151 + let output = match result { 152 + Ok(output) => { 153 + println!("[Tool result: {} bytes]", output.len()); 154 + output 155 + } 156 + Err(e) => { 157 + println!("[Tool error: {}]", e); 158 + format!("Error: {}", e) 159 + } 160 + }; 161 + 162 + // Add tool result to conversation 163 + self.conversation.push(Message { 164 + role: Role::User, 165 + content: format!( 166 + "Tool result for {}:\n{}", 167 + tool_call.name, output 168 + ), 169 + }); 170 + } 171 + 172 + // Continue the loop to get the next LLM response 173 + } 174 + } 175 + } 176 + } 177 + } 178 + 179 + const PLANNING_SYSTEM_PROMPT: &str = r#"You are a planning agent that helps users create detailed specifications for software development tasks. 180 + 181 + Your role is to: 182 + 1. Ask clarifying questions to understand the user's requirements 183 + 2. Break down complex tasks into smaller, manageable subtasks 184 + 3. Define clear acceptance criteria for each task 185 + 4. Create a structured specification file in JSON format 186 + 187 + When the user provides their requirements, you should: 188 + - Ask about technical constraints, dependencies, and environment 189 + - Identify risks and edge cases 190 + - Suggest best practices and design patterns 191 + - Help define test criteria 192 + 193 + Use the available tools to: 194 + - read_file: Read existing files to understand the codebase 195 + - write_file: Create the specification file 196 + - list_files: Explore the project structure 197 + - run_command: Run commands to gather information about the environment 198 + 199 + When you have enough information, create a spec.json file with this structure: 200 + { 201 + "name": "project-name", 202 + "description": "Brief description of the project", 203 + "branch_name": "feature/branch-name", 204 + "created_at": "2024-01-01T00:00:00Z", 205 + "tasks": [ 206 + { 207 + "id": "task-1", 208 + "title": "Task title", 209 + "description": "Detailed task description", 210 + "acceptance_criteria": [ 211 + "Criterion 1", 212 + "Criterion 2" 213 + ], 214 + "status": "pending" 215 + } 216 + ], 217 + "learnings": [] 218 + } 219 + 220 + Be thorough but efficient. Ask questions one at a time to avoid overwhelming the user. 221 + "#;
+27
tests/planning_test.rs
··· 1 + use rustagent::planning::PlanningAgent; 2 + use rustagent::config::Config; 3 + use tempfile::TempDir; 4 + use std::fs; 5 + 6 + #[tokio::test] 7 + async fn test_planning_agent_creation() { 8 + let temp = TempDir::new().unwrap(); 9 + let config_path = temp.path().join("config.toml"); 10 + 11 + fs::write(&config_path, r#" 12 + [llm] 13 + provider = "anthropic" 14 + model = "claude-sonnet-4-20250514" 15 + 16 + [anthropic] 17 + api_key = "test-key" 18 + 19 + [rustagent] 20 + spec_dir = "specs" 21 + "#).unwrap(); 22 + 23 + let config = Config::load(&config_path).unwrap(); 24 + let agent = PlanningAgent::new(config, temp.path().to_str().unwrap().to_string()); 25 + 26 + assert!(agent.spec_dir.ends_with(temp.path().to_str().unwrap())); 27 + }