An AI agent built to do Ralph loops - plan mode for planning and ralph mode for implementing.
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}