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

refactor: make tool registry thread-safe with Arc<RwLock>

- Use Arc<RwLock<HashMap>> for concurrent access
- Tools stored as Arc<dyn Tool> for sharing
- Registry is now Clone (just clones the Arc)
- Safe to use across multiple threads
- Add concurrency tests
- Remove mut from registry declarations (register no longer needs &mut)

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>

+106 -41
+6 -5
src/planning/mod.rs
··· 3 3 use crate::llm::anthropic::AnthropicClient; 4 4 use crate::tools::{ToolRegistry, file::{ReadFileTool, WriteFileTool, ListFilesTool}, shell::RunCommandTool}; 5 5 use std::io::{self, Write}; 6 + use std::sync::Arc; 6 7 7 8 /// Planning agent for interactive spec creation 8 9 pub struct PlanningAgent { ··· 28 29 }; 29 30 30 31 // 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)); 32 + let registry = ToolRegistry::new(); 33 + registry.register(Arc::new(ReadFileTool)); 34 + registry.register(Arc::new(WriteFileTool)); 35 + registry.register(Arc::new(ListFilesTool)); 36 + registry.register(Arc::new(RunCommandTool)); 36 37 37 38 // Initialize conversation with system message 38 39 let system_message = Message {
+5 -5
src/ralph/mod.rs
··· 33 33 }; 34 34 35 35 // Register tools 36 - let mut tools = ToolRegistry::new(); 37 - tools.register(Box::new(ReadFileTool)); 38 - tools.register(Box::new(WriteFileTool)); 39 - tools.register(Box::new(ListFilesTool)); 40 - tools.register(Box::new(RunCommandTool)); 36 + let tools = ToolRegistry::new(); 37 + tools.register(Arc::new(ReadFileTool)); 38 + tools.register(Arc::new(WriteFileTool)); 39 + tools.register(Arc::new(ListFilesTool)); 40 + tools.register(Arc::new(RunCommandTool)); 41 41 42 42 let max_iterations = max_iterations 43 43 .or(config.rustagent.max_iterations)
+23 -11
src/tools/mod.rs
··· 1 - use async_trait::async_trait; 2 1 use anyhow::Result; 2 + use async_trait::async_trait; 3 3 use std::collections::HashMap; 4 + use std::sync::{Arc, RwLock}; 4 5 5 6 use crate::llm::ToolDefinition; 6 7 ··· 22 23 23 24 /// Registry for managing available tools 24 25 pub struct ToolRegistry { 25 - tools: HashMap<String, Box<dyn Tool>>, 26 + tools: Arc<RwLock<HashMap<String, Arc<dyn Tool>>>>, 26 27 } 27 28 28 29 impl ToolRegistry { 29 30 /// Creates a new empty tool registry 30 31 pub fn new() -> Self { 31 32 Self { 32 - tools: HashMap::new(), 33 + tools: Arc::new(RwLock::new(HashMap::new())), 33 34 } 34 35 } 35 36 36 37 /// Registers a new tool in the registry 37 - pub fn register(&mut self, tool: Box<dyn Tool>) { 38 - let name = tool.name().to_string(); 39 - self.tools.insert(name, tool); 38 + pub fn register(&self, tool: Arc<dyn Tool>) { 39 + let mut tools = self.tools.write().unwrap(); 40 + tools.insert(tool.name().to_string(), tool); 40 41 } 41 42 42 43 /// Gets a tool by name 43 - pub fn get(&self, name: &str) -> Option<&dyn Tool> { 44 - self.tools.get(name).map(|b| &**b) 44 + pub fn get(&self, name: &str) -> Option<Arc<dyn Tool>> { 45 + let tools = self.tools.read().unwrap(); 46 + tools.get(name).cloned() 45 47 } 46 48 47 49 /// Lists all registered tool names 48 - pub fn list(&self) -> Vec<&str> { 49 - self.tools.keys().map(|s| s.as_str()).collect() 50 + pub fn list(&self) -> Vec<String> { 51 + let tools = self.tools.read().unwrap(); 52 + tools.keys().cloned().collect() 50 53 } 51 54 52 55 /// Converts all registered tools to LLM tool definitions 53 56 pub fn definitions(&self) -> Vec<ToolDefinition> { 54 - self.tools 57 + let tools = self.tools.read().unwrap(); 58 + tools 55 59 .values() 56 60 .map(|tool| ToolDefinition { 57 61 name: tool.name().to_string(), ··· 59 63 parameters: tool.parameters(), 60 64 }) 61 65 .collect() 66 + } 67 + } 68 + 69 + impl Clone for ToolRegistry { 70 + fn clone(&self) -> Self { 71 + Self { 72 + tools: Arc::clone(&self.tools), 73 + } 62 74 } 63 75 } 64 76
+72 -20
tests/tools_test.rs
··· 1 - use rustagent::tools::{Tool, ToolRegistry}; 2 - use async_trait::async_trait; 3 1 use anyhow::Result; 2 + use async_trait::async_trait; 3 + use rustagent::tools::{Tool, ToolRegistry}; 4 4 5 5 struct MockTool; 6 6 ··· 28 28 29 29 #[tokio::test] 30 30 async fn test_tool_registry() { 31 - let mut registry = ToolRegistry::new(); 32 - registry.register(Box::new(MockTool)); 31 + let registry = ToolRegistry::new(); 32 + registry.register(Arc::new(MockTool)); 33 33 34 34 assert!(registry.get("mock_tool").is_some()); 35 35 assert!(registry.get("nonexistent").is_none()); ··· 42 42 assert_eq!(result, "success"); 43 43 } 44 44 45 - use rustagent::tools::file::{ReadFileTool, WriteFileTool, ListFilesTool}; 45 + use rustagent::tools::file::{ListFilesTool, ReadFileTool, WriteFileTool}; 46 46 use rustagent::tools::shell::RunCommandTool; 47 47 use serde_json::json; 48 - use tempfile::TempDir; 49 48 use std::fs; 49 + use tempfile::TempDir; 50 50 51 51 #[tokio::test] 52 52 async fn test_read_file_tool() { ··· 55 55 fs::write(&file_path, "hello world").unwrap(); 56 56 57 57 let tool = ReadFileTool; 58 - let result = tool.execute(json!({ 59 - "path": file_path.to_str().unwrap() 60 - })).await.unwrap(); 58 + let result = tool 59 + .execute(json!({ 60 + "path": file_path.to_str().unwrap() 61 + })) 62 + .await 63 + .unwrap(); 61 64 62 65 assert!(result.contains("hello world")); 63 66 } ··· 71 74 tool.execute(json!({ 72 75 "path": file_path.to_str().unwrap(), 73 76 "content": "test content" 74 - })).await.unwrap(); 77 + })) 78 + .await 79 + .unwrap(); 75 80 76 81 let content = fs::read_to_string(&file_path).unwrap(); 77 82 assert_eq!(content, "test content"); ··· 84 89 fs::write(temp.path().join("file2.txt"), "b").unwrap(); 85 90 86 91 let tool = ListFilesTool; 87 - let result = tool.execute(json!({ 88 - "path": temp.path().to_str().unwrap() 89 - })).await.unwrap(); 92 + let result = tool 93 + .execute(json!({ 94 + "path": temp.path().to_str().unwrap() 95 + })) 96 + .await 97 + .unwrap(); 90 98 91 99 assert!(result.contains("file1.txt")); 92 100 assert!(result.contains("file2.txt")); ··· 95 103 #[tokio::test] 96 104 async fn test_run_command_tool() { 97 105 let tool = RunCommandTool; 98 - let result = tool.execute(json!({ 99 - "command": "echo hello" 100 - })).await.unwrap(); 106 + let result = tool 107 + .execute(json!({ 108 + "command": "echo hello" 109 + })) 110 + .await 111 + .unwrap(); 101 112 102 113 assert!(result.contains("hello")); 103 114 } ··· 107 118 let temp = TempDir::new().unwrap(); 108 119 109 120 let tool = RunCommandTool; 110 - let result = tool.execute(json!({ 111 - "command": "pwd", 112 - "working_dir": temp.path().to_str().unwrap() 113 - })).await.unwrap(); 121 + let result = tool 122 + .execute(json!({ 123 + "command": "pwd", 124 + "working_dir": temp.path().to_str().unwrap() 125 + })) 126 + .await 127 + .unwrap(); 114 128 115 129 assert!(result.contains(temp.path().to_str().unwrap())); 116 130 } 131 + 132 + use std::sync::Arc; 133 + use std::thread; 134 + 135 + #[test] 136 + fn test_registry_clone_and_concurrent_access() { 137 + let mut registry = ToolRegistry::new(); 138 + registry.register(Arc::new(MockTool)); 139 + 140 + let registry1 = registry.clone(); 141 + let registry2 = registry.clone(); 142 + 143 + let handle1 = thread::spawn(move || { 144 + registry1.get("mock_tool").is_some() 145 + }); 146 + 147 + let handle2 = thread::spawn(move || { 148 + registry2.get("mock_tool").is_some() 149 + }); 150 + 151 + assert!(handle1.join().unwrap()); 152 + assert!(handle2.join().unwrap()); 153 + } 154 + 155 + #[test] 156 + fn test_registry_register_while_reading() { 157 + let registry = ToolRegistry::new(); 158 + let registry_clone = registry.clone(); 159 + 160 + let handle = thread::spawn(move || { 161 + registry_clone.register(Arc::new(MockTool)); 162 + }); 163 + 164 + // Should be able to read while another thread is registering 165 + let _ = registry.list(); 166 + 167 + handle.join().unwrap(); 168 + }