An AI agent built to do Ralph loops - plan mode for planning and ralph mode for implementing.
at new-directions 400 lines 14 kB view raw
1use serde::{Deserialize, Serialize}; 2 3#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] 4#[serde(rename_all = "lowercase")] 5pub enum AutonomyLevel { 6 Full, 7 #[default] 8 Supervised, 9 Gated, 10} 11 12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] 13#[serde(rename_all = "snake_case")] 14pub enum ApprovalGate { 15 PlanReview, 16 PreCommit, 17 TaskComplete, 18 DecisionPoint, 19 GoalComplete, 20 WorkerSpawn, 21} 22 23impl AutonomyLevel { 24 /// Returns the set of gates that are active for this autonomy level. 25 pub fn active_gates(&self) -> Vec<ApprovalGate> { 26 match self { 27 Self::Full => vec![], 28 Self::Supervised => vec![ 29 ApprovalGate::PlanReview, 30 ApprovalGate::PreCommit, 31 ApprovalGate::TaskComplete, 32 ApprovalGate::GoalComplete, 33 ], 34 Self::Gated => vec![ 35 ApprovalGate::PlanReview, 36 ApprovalGate::PreCommit, 37 ApprovalGate::TaskComplete, 38 ApprovalGate::DecisionPoint, 39 ApprovalGate::GoalComplete, 40 ApprovalGate::WorkerSpawn, 41 ], 42 } 43 } 44 45 /// Check whether a specific gate is active for this autonomy level. 46 pub fn is_gate_active(&self, gate: ApprovalGate) -> bool { 47 self.active_gates().contains(&gate) 48 } 49} 50 51#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 52pub struct ApprovalRequest { 53 /// Unique ID for this request 54 pub id: String, 55 /// Which gate triggered this request 56 pub gate: ApprovalGate, 57 /// Human-readable summary of what's being approved 58 pub context_summary: String, 59 /// Description of the proposed action 60 pub proposed_action: String, 61 /// ID of the goal/task this relates to 62 pub related_node_id: String, 63} 64 65#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 66#[serde(tag = "action", rename_all = "lowercase")] 67pub enum ApprovalResponse { 68 Approve, 69 Reject { reason: String }, 70 Modify { instructions: String }, 71} 72 73/// Checks whether a gate requires approval for the current autonomy level. 74pub struct GateChecker { 75 level: AutonomyLevel, 76} 77 78impl GateChecker { 79 pub fn new(level: AutonomyLevel) -> Self { 80 Self { level } 81 } 82 83 /// Check if a gate requires approval. Returns Some(ApprovalRequest) if the gate 84 /// is active and approval is needed, None if the gate is inactive. 85 pub fn check_gate( 86 &self, 87 gate: ApprovalGate, 88 related_node_id: &str, 89 context_summary: &str, 90 proposed_action: &str, 91 ) -> Option<ApprovalRequest> { 92 if self.level.is_gate_active(gate) { 93 Some(ApprovalRequest { 94 id: format!( 95 "approval-{}", 96 uuid::Uuid::new_v4() 97 .simple() 98 .to_string() 99 .get(..8) 100 .unwrap_or("00000000") 101 ), 102 gate, 103 context_summary: context_summary.to_string(), 104 proposed_action: proposed_action.to_string(), 105 related_node_id: related_node_id.to_string(), 106 }) 107 } else { 108 None 109 } 110 } 111} 112 113#[cfg(test)] 114mod tests { 115 use super::*; 116 117 // ===== Task 1: AutonomyLevel and ApprovalGate types ===== 118 119 #[test] 120 fn test_full_autonomy_has_no_active_gates() { 121 let level = AutonomyLevel::Full; 122 assert_eq!(level.active_gates(), vec![]); 123 } 124 125 #[test] 126 fn test_supervised_autonomy_has_correct_gates() { 127 let level = AutonomyLevel::Supervised; 128 let gates = level.active_gates(); 129 assert_eq!(gates.len(), 4); 130 assert!(gates.contains(&ApprovalGate::PlanReview)); 131 assert!(gates.contains(&ApprovalGate::PreCommit)); 132 assert!(gates.contains(&ApprovalGate::TaskComplete)); 133 assert!(gates.contains(&ApprovalGate::GoalComplete)); 134 } 135 136 #[test] 137 fn test_gated_autonomy_has_all_gates() { 138 let level = AutonomyLevel::Gated; 139 let gates = level.active_gates(); 140 assert_eq!(gates.len(), 6); 141 assert!(gates.contains(&ApprovalGate::PlanReview)); 142 assert!(gates.contains(&ApprovalGate::PreCommit)); 143 assert!(gates.contains(&ApprovalGate::TaskComplete)); 144 assert!(gates.contains(&ApprovalGate::DecisionPoint)); 145 assert!(gates.contains(&ApprovalGate::GoalComplete)); 146 assert!(gates.contains(&ApprovalGate::WorkerSpawn)); 147 } 148 149 #[test] 150 fn test_default_autonomy_level_is_supervised() { 151 assert_eq!(AutonomyLevel::default(), AutonomyLevel::Supervised); 152 } 153 154 #[test] 155 fn test_is_gate_active() { 156 let full = AutonomyLevel::Full; 157 assert!(!full.is_gate_active(ApprovalGate::PlanReview)); 158 159 let supervised = AutonomyLevel::Supervised; 160 assert!(supervised.is_gate_active(ApprovalGate::PlanReview)); 161 assert!(supervised.is_gate_active(ApprovalGate::PreCommit)); 162 assert!(supervised.is_gate_active(ApprovalGate::TaskComplete)); 163 assert!(supervised.is_gate_active(ApprovalGate::GoalComplete)); 164 assert!(!supervised.is_gate_active(ApprovalGate::DecisionPoint)); 165 assert!(!supervised.is_gate_active(ApprovalGate::WorkerSpawn)); 166 167 let gated = AutonomyLevel::Gated; 168 assert!(gated.is_gate_active(ApprovalGate::PlanReview)); 169 assert!(gated.is_gate_active(ApprovalGate::DecisionPoint)); 170 assert!(gated.is_gate_active(ApprovalGate::WorkerSpawn)); 171 } 172 173 // ===== Task 2 & 3: ApprovalRequest, ApprovalResponse, and serde tests ===== 174 175 #[test] 176 fn test_approval_request_construction() { 177 let request = ApprovalRequest { 178 id: "approval-12345678".to_string(), 179 gate: ApprovalGate::PlanReview, 180 context_summary: "Test context".to_string(), 181 proposed_action: "Test action".to_string(), 182 related_node_id: "ra-1234".to_string(), 183 }; 184 185 assert_eq!(request.id, "approval-12345678"); 186 assert_eq!(request.gate, ApprovalGate::PlanReview); 187 assert_eq!(request.context_summary, "Test context"); 188 assert_eq!(request.proposed_action, "Test action"); 189 assert_eq!(request.related_node_id, "ra-1234"); 190 } 191 192 #[test] 193 fn test_autonomy_level_serde_roundtrip() { 194 let levels = vec![ 195 AutonomyLevel::Full, 196 AutonomyLevel::Supervised, 197 AutonomyLevel::Gated, 198 ]; 199 200 for level in levels { 201 let json = serde_json::to_string(&level).expect("failed to serialize"); 202 let deserialized: AutonomyLevel = 203 serde_json::from_str(&json).expect("failed to deserialize"); 204 assert_eq!(level, deserialized); 205 } 206 } 207 208 #[test] 209 fn test_autonomy_level_serde_format() { 210 let full_json = serde_json::to_string(&AutonomyLevel::Full).unwrap(); 211 assert_eq!(full_json, "\"full\""); 212 213 let supervised_json = serde_json::to_string(&AutonomyLevel::Supervised).unwrap(); 214 assert_eq!(supervised_json, "\"supervised\""); 215 216 let gated_json = serde_json::to_string(&AutonomyLevel::Gated).unwrap(); 217 assert_eq!(gated_json, "\"gated\""); 218 } 219 220 #[test] 221 fn test_approval_gate_serde_roundtrip() { 222 let gates = vec![ 223 ApprovalGate::PlanReview, 224 ApprovalGate::PreCommit, 225 ApprovalGate::TaskComplete, 226 ApprovalGate::DecisionPoint, 227 ApprovalGate::GoalComplete, 228 ApprovalGate::WorkerSpawn, 229 ]; 230 231 for gate in gates { 232 let json = serde_json::to_string(&gate).expect("failed to serialize"); 233 let deserialized: ApprovalGate = 234 serde_json::from_str(&json).expect("failed to deserialize"); 235 assert_eq!(gate, deserialized); 236 } 237 } 238 239 #[test] 240 fn test_approval_gate_serde_format() { 241 let json = serde_json::to_string(&ApprovalGate::PlanReview).unwrap(); 242 assert_eq!(json, "\"plan_review\""); 243 244 let json = serde_json::to_string(&ApprovalGate::PreCommit).unwrap(); 245 assert_eq!(json, "\"pre_commit\""); 246 247 let json = serde_json::to_string(&ApprovalGate::TaskComplete).unwrap(); 248 assert_eq!(json, "\"task_complete\""); 249 250 let json = serde_json::to_string(&ApprovalGate::DecisionPoint).unwrap(); 251 assert_eq!(json, "\"decision_point\""); 252 253 let json = serde_json::to_string(&ApprovalGate::GoalComplete).unwrap(); 254 assert_eq!(json, "\"goal_complete\""); 255 256 let json = serde_json::to_string(&ApprovalGate::WorkerSpawn).unwrap(); 257 assert_eq!(json, "\"worker_spawn\""); 258 } 259 260 #[test] 261 fn test_approval_response_approve_serde() { 262 let response = ApprovalResponse::Approve; 263 let json = serde_json::to_string(&response).expect("failed to serialize"); 264 let deserialized: ApprovalResponse = 265 serde_json::from_str(&json).expect("failed to deserialize"); 266 assert_eq!(json, "{\"action\":\"approve\"}"); 267 assert_eq!(deserialized, response); 268 } 269 270 #[test] 271 fn test_approval_response_reject_serde() { 272 let response = ApprovalResponse::Reject { 273 reason: "Too risky".to_string(), 274 }; 275 let json = serde_json::to_string(&response).expect("failed to serialize"); 276 let deserialized: ApprovalResponse = 277 serde_json::from_str(&json).expect("failed to deserialize"); 278 assert!(json.contains("\"action\":\"reject\"")); 279 assert!(json.contains("\"reason\":\"Too risky\"")); 280 assert_eq!(deserialized, response); 281 } 282 283 #[test] 284 fn test_approval_response_modify_serde() { 285 let response = ApprovalResponse::Modify { 286 instructions: "Change the plan".to_string(), 287 }; 288 let json = serde_json::to_string(&response).expect("failed to serialize"); 289 let deserialized: ApprovalResponse = 290 serde_json::from_str(&json).expect("failed to deserialize"); 291 assert!(json.contains("\"action\":\"modify\"")); 292 assert!(json.contains("\"instructions\":\"Change the plan\"")); 293 assert_eq!(deserialized, response); 294 } 295 296 #[test] 297 fn test_approval_request_serde() { 298 let request = ApprovalRequest { 299 id: "approval-12345678".to_string(), 300 gate: ApprovalGate::PlanReview, 301 context_summary: "Planning a new feature".to_string(), 302 proposed_action: "Create task nodes".to_string(), 303 related_node_id: "ra-abcd".to_string(), 304 }; 305 306 let json = serde_json::to_string(&request).expect("failed to serialize"); 307 let deserialized: ApprovalRequest = 308 serde_json::from_str(&json).expect("failed to deserialize"); 309 310 assert_eq!(deserialized.id, request.id); 311 assert_eq!(deserialized.gate, request.gate); 312 assert_eq!(deserialized.context_summary, request.context_summary); 313 assert_eq!(deserialized.proposed_action, request.proposed_action); 314 assert_eq!(deserialized.related_node_id, request.related_node_id); 315 } 316 317 // ===== Task 4: GateChecker tests ===== 318 319 #[test] 320 fn test_gate_checker_full_autonomy_returns_none() { 321 let checker = GateChecker::new(AutonomyLevel::Full); 322 let result = checker.check_gate( 323 ApprovalGate::PlanReview, 324 "ra-1234", 325 "Test context", 326 "Test action", 327 ); 328 assert_eq!(result, None); 329 } 330 331 #[test] 332 fn test_gate_checker_supervised_plan_review_returns_request() { 333 let checker = GateChecker::new(AutonomyLevel::Supervised); 334 let result = checker.check_gate( 335 ApprovalGate::PlanReview, 336 "ra-1234", 337 "Planning phase", 338 "Create subtasks", 339 ); 340 341 assert!(result.is_some()); 342 let req = result.unwrap(); 343 assert_eq!(req.gate, ApprovalGate::PlanReview); 344 assert_eq!(req.context_summary, "Planning phase"); 345 assert_eq!(req.proposed_action, "Create subtasks"); 346 assert_eq!(req.related_node_id, "ra-1234"); 347 assert!(req.id.starts_with("approval-")); 348 } 349 350 #[test] 351 fn test_gate_checker_supervised_inactive_gate_returns_none() { 352 let checker = GateChecker::new(AutonomyLevel::Supervised); 353 let result = checker.check_gate( 354 ApprovalGate::DecisionPoint, 355 "ra-1234", 356 "Test context", 357 "Test action", 358 ); 359 assert_eq!(result, None); 360 } 361 362 #[test] 363 fn test_gate_checker_gated_all_gates_active() { 364 let checker = GateChecker::new(AutonomyLevel::Gated); 365 366 let gates = vec![ 367 ApprovalGate::PlanReview, 368 ApprovalGate::PreCommit, 369 ApprovalGate::TaskComplete, 370 ApprovalGate::DecisionPoint, 371 ApprovalGate::GoalComplete, 372 ApprovalGate::WorkerSpawn, 373 ]; 374 375 for gate in gates { 376 let result = checker.check_gate(gate, "ra-1234", "Test context", "Test action"); 377 assert!( 378 result.is_some(), 379 "Gate {:?} should be active in Gated mode", 380 gate 381 ); 382 assert_eq!(result.unwrap().gate, gate); 383 } 384 } 385 386 #[test] 387 fn test_gate_checker_generates_unique_ids() { 388 let checker = GateChecker::new(AutonomyLevel::Supervised); 389 390 let req1 = checker 391 .check_gate(ApprovalGate::PlanReview, "ra-1234", "Test 1", "Action 1") 392 .unwrap(); 393 394 let req2 = checker 395 .check_gate(ApprovalGate::PlanReview, "ra-5678", "Test 2", "Action 2") 396 .unwrap(); 397 398 assert_ne!(req1.id, req2.id); 399 } 400}