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

refactor: replace panics with proper error handling

- Replace expect() with ok_or_else() and descriptive errors
- Replace panic!() with anyhow::bail()
- Use per-mode LLM config (planning_llm, ralph_llm)
- Provide helpful error messages for missing configs
- More robust error handling for unsupported providers
- Update new() methods to return Result
- Replace expect("Task should exist") with context()

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

+45 -31
+2 -2
src/main.rs
··· 85 85 .unwrap_or_else(|| config.rustagent.spec_dir.clone()); 86 86 87 87 // Create and run planning agent 88 - let mut agent = planning::PlanningAgent::new(config, dir); 88 + let mut agent = planning::PlanningAgent::new(config, dir)?; 89 89 agent.run().await?; 90 90 } 91 91 Commands::Run { spec_file, max_iterations } => { ··· 94 94 let config = config::Config::load(&config_path)?; 95 95 96 96 // Create and run Ralph loop 97 - let ralph = ralph::RalphLoop::new(config, spec_file.clone(), *max_iterations); 97 + let ralph = ralph::RalphLoop::new(config, spec_file.clone(), *max_iterations)?; 98 98 ralph.run().await?; 99 99 } 100 100 }
+18 -11
src/planning/mod.rs
··· 16 16 17 17 impl PlanningAgent { 18 18 /// Create a new planning agent with the given config 19 - pub fn new(config: Config, spec_dir: String) -> Self { 19 + pub fn new(config: Config, spec_dir: String) -> anyhow::Result<Self> { 20 20 // Get planning-specific LLM config 21 21 let llm_config = config.planning_llm().clone(); 22 22 23 23 // Create LLM client based on provider 24 24 let client: Box<dyn LlmClient> = match llm_config.provider { 25 25 LlmProvider::Anthropic => { 26 - let api_key = config 27 - .anthropic 28 - .expect("Anthropic config required for Anthropic provider") 29 - .api_key; 26 + let anthropic_config = config.anthropic 27 + .as_ref() 28 + .ok_or_else(|| anyhow::anyhow!( 29 + "Anthropic provider selected but [anthropic] config missing" 30 + ))?; 31 + 30 32 Box::new(AnthropicClient::new( 31 - api_key, 32 - llm_config.model, 33 + anthropic_config.api_key.clone(), 34 + llm_config.model.clone(), 33 35 llm_config.max_tokens, 34 36 )) 35 37 } 36 - _ => panic!("Unsupported LLM provider"), 38 + LlmProvider::OpenAi => { 39 + anyhow::bail!("OpenAI provider not yet implemented") 40 + } 41 + LlmProvider::Ollama => { 42 + anyhow::bail!("Ollama provider not yet implemented") 43 + } 37 44 }; 38 45 39 46 // Create security validator and permission handler 40 - let validator = Arc::new(SecurityValidator::new(config.security.clone()).expect("Failed to create security validator")); 47 + let validator = Arc::new(SecurityValidator::new(config.security.clone())?); 41 48 let permission_handler = Arc::new(CliPermissionHandler); 42 49 43 50 // Create and populate tool registry ··· 65 72 content: PLANNING_SYSTEM_PROMPT.to_string(), 66 73 }; 67 74 68 - Self { 75 + Ok(Self { 69 76 client, 70 77 registry, 71 78 spec_dir, 72 79 conversation: vec![system_message], 73 - } 80 + }) 74 81 } 75 82 76 83 /// Run the interactive planning loop
+23 -16
src/ralph/mod.rs
··· 20 20 } 21 21 22 22 impl RalphLoop { 23 - pub fn new(config: Config, spec_path: String, max_iterations: Option<usize>) -> Self { 23 + pub fn new(config: Config, spec_path: String, max_iterations: Option<usize>) -> Result<Self> { 24 24 // Get ralph-specific LLM config 25 25 let llm_config = config.ralph_llm().clone(); 26 26 27 27 // Create LLM client based on provider 28 28 let client: Arc<dyn LlmClient> = match llm_config.provider { 29 29 LlmProvider::Anthropic => { 30 - let api_key = config 31 - .anthropic 32 - .expect("Anthropic config required") 33 - .api_key; 30 + let anthropic_config = config.anthropic 31 + .as_ref() 32 + .ok_or_else(|| anyhow::anyhow!( 33 + "Anthropic provider selected but [anthropic] config missing" 34 + ))?; 35 + 34 36 Arc::new(AnthropicClient::new( 35 - api_key, 36 - llm_config.model, 37 + anthropic_config.api_key.clone(), 38 + llm_config.model.clone(), 37 39 llm_config.max_tokens, 38 40 )) 39 41 } 40 - _ => panic!("Only Anthropic provider is currently supported"), 42 + LlmProvider::OpenAi => { 43 + anyhow::bail!("OpenAI provider not yet implemented") 44 + } 45 + LlmProvider::Ollama => { 46 + anyhow::bail!("Ollama provider not yet implemented") 47 + } 41 48 }; 42 49 43 50 // Create security validator and permission handler 44 - let validator = Arc::new(SecurityValidator::new(config.security.clone()).expect("Failed to create security validator")); 51 + let validator = Arc::new(SecurityValidator::new(config.security.clone())?); 45 52 let permission_handler = Arc::new(CliPermissionHandler); 46 53 47 54 // Register tools ··· 67 74 .or(config.rustagent.max_iterations) 68 75 .unwrap_or(DEFAULT_MAX_ITERATIONS); 69 76 70 - Self { 77 + Ok(Self { 71 78 client, 72 79 tools, 73 80 spec_path, 74 81 max_iterations, 75 - } 82 + }) 76 83 } 77 84 78 85 pub async fn run(&self) -> Result<()> { ··· 110 117 { 111 118 let task_mut = spec 112 119 .find_task_mut(&task.id) 113 - .expect("Task should exist"); 120 + .context("Task not found in spec")?; 114 121 task_mut.status = TaskStatus::InProgress; 115 122 } 116 123 spec.save(&self.spec_path) ··· 127 134 println!("Task completed successfully"); 128 135 let task_mut = spec 129 136 .find_task_mut(&task.id) 130 - .expect("Task should exist"); 137 + .context("Task not found in spec")?; 131 138 task_mut.status = TaskStatus::Complete; 132 139 task_mut.completed_at = Some(Utc::now()); 133 140 spec.save(&self.spec_path)?; ··· 136 143 println!("Task is blocked"); 137 144 let task_mut = spec 138 145 .find_task_mut(&task.id) 139 - .expect("Task should exist"); 146 + .context("Task not found in spec")?; 140 147 task_mut.status = TaskStatus::Blocked; 141 148 spec.save(&self.spec_path)?; 142 149 } ··· 145 152 // Reset to pending to retry 146 153 let task_mut = spec 147 154 .find_task_mut(&task.id) 148 - .expect("Task should exist"); 155 + .context("Task not found in spec")?; 149 156 task_mut.status = TaskStatus::Pending; 150 157 spec.save(&self.spec_path)?; 151 158 } ··· 157 164 let mut spec = Spec::load(&self.spec_path)?; 158 165 let task_mut = spec 159 166 .find_task_mut(&task.id) 160 - .expect("Task should exist"); 167 + .context("Task not found in spec")?; 161 168 task_mut.status = TaskStatus::Pending; 162 169 spec.save(&self.spec_path)?; 163 170 break;
+1 -1
tests/planning_test.rs
··· 21 21 "#).unwrap(); 22 22 23 23 let config = Config::load(&config_path).unwrap(); 24 - let agent = PlanningAgent::new(config, temp.path().to_str().unwrap().to_string()); 24 + let agent = PlanningAgent::new(config, temp.path().to_str().unwrap().to_string()).unwrap(); 25 25 26 26 assert!(agent.spec_dir.ends_with(temp.path().to_str().unwrap())); 27 27 }
+1 -1
tests/ralph_test.rs
··· 34 34 spec.save(&spec_path).unwrap(); 35 35 36 36 let config = Config::load(&config_path).unwrap(); 37 - let ralph = RalphLoop::new(config, spec_path.to_str().unwrap().to_string(), None); 37 + let ralph = RalphLoop::new(config, spec_path.to_str().unwrap().to_string(), None).unwrap(); 38 38 39 39 assert!(ralph.spec_path.ends_with("test.json")); 40 40 }