1use std::sync::atomic::{AtomicBool, Ordering};
15use std::sync::{Arc, RwLock};
16
17use async_trait::async_trait;
18use serde_json::{json, Value};
19use tokio::sync::Notify;
20
21use crate::error::Result;
22use crate::event::{AgentEvent, EventSink};
23use crate::llm::ToolSpec;
24use crate::permissions::{PermissionMode, PermissionsConfig};
25use crate::tools::{Tool, ToolSideEffect};
26
27#[derive(Debug, Clone)]
33pub enum PlanApprovalResult {
34 Approved,
36 Rejected { reason: String },
38}
39
40pub struct PlanApprovalGate {
53 pub exploring_plan_mode: Arc<AtomicBool>,
55 pub pending_plan: Arc<RwLock<Option<String>>>,
57 response: Arc<RwLock<Option<PlanApprovalResult>>>,
59 notify: Arc<Notify>,
61}
62
63impl PlanApprovalGate {
64 pub fn new() -> Self {
66 Self {
67 exploring_plan_mode: Arc::new(AtomicBool::new(false)),
68 pending_plan: Arc::new(RwLock::new(None)),
69 response: Arc::new(RwLock::new(None)),
70 notify: Arc::new(Notify::new()),
71 }
72 }
73
74 pub async fn wait_for_approval(&self) -> PlanApprovalResult {
80 loop {
81 let result_opt = {
83 let guard = self
84 .response
85 .read()
86 .expect("PlanApprovalGate response lock poisoned");
87 guard.clone()
88 };
89 if let Some(result) = result_opt {
90 if let Ok(mut w) = self.response.write() {
92 *w = None;
93 }
94 return result;
95 }
96 self.notify.notified().await;
98 }
99 }
100
101 pub fn approve(&self) {
103 if let Ok(mut w) = self.response.write() {
104 *w = Some(PlanApprovalResult::Approved);
105 }
106 self.notify.notify_one();
107 }
108
109 pub fn reject(&self, reason: impl Into<String>) {
111 if let Ok(mut w) = self.response.write() {
112 *w = Some(PlanApprovalResult::Rejected {
113 reason: reason.into(),
114 });
115 }
116 self.notify.notify_one();
117 }
118}
119
120impl Default for PlanApprovalGate {
121 fn default() -> Self {
122 Self::new()
123 }
124}
125
126pub struct EnterPlanModeTool {
140 gate: Arc<PlanApprovalGate>,
141 permissions: Option<Arc<PermissionsConfig>>,
143}
144
145impl EnterPlanModeTool {
146 pub fn new(gate: Arc<PlanApprovalGate>) -> Self {
148 Self {
149 gate,
150 permissions: None,
151 }
152 }
153
154 pub fn with_permissions(mut self, permissions: Arc<PermissionsConfig>) -> Self {
157 self.permissions = Some(permissions);
158 self
159 }
160}
161
162#[async_trait]
163impl Tool for EnterPlanModeTool {
164 fn spec(&self) -> ToolSpec {
165 ToolSpec {
166 name: "enter_plan_mode".into(),
167 description: "Enter read-only planning mode. While active, write tools (write_file, \
168 apply_patch, run_shell, …) are blocked. Use read tools to explore the \
169 codebase freely, then call exit_plan_mode with a markdown plan summary."
170 .into(),
171 parameters: json!({
172 "type": "object",
173 "properties": {}
174 }),
175 }
176 }
177
178 async fn execute(&self, _arguments: Value) -> Result<String> {
179 self.gate.exploring_plan_mode.store(true, Ordering::Relaxed);
180
181 let mut response = json!({ "entered": true });
183 if let Some(ref config) = self.permissions {
184 if matches!(config.mode, PermissionMode::Plan { .. }) {
185 response["default_mode"] = json!("plan");
186 }
187 }
188
189 Ok(response.to_string())
190 }
191
192 fn side_effect_class(&self) -> ToolSideEffect {
193 ToolSideEffect::Mutating
194 }
195
196 fn is_readonly(&self) -> bool {
197 false
198 }
199}
200
201pub struct ExitPlanModeTool {
212 gate: Arc<PlanApprovalGate>,
213 event_sink: Arc<dyn EventSink>,
214 permissions: Option<Arc<PermissionsConfig>>,
216}
217
218impl ExitPlanModeTool {
219 pub fn new(gate: Arc<PlanApprovalGate>, event_sink: Arc<dyn EventSink>) -> Self {
224 Self {
225 gate,
226 event_sink,
227 permissions: None,
228 }
229 }
230
231 pub fn with_permissions(mut self, permissions: Arc<PermissionsConfig>) -> Self {
234 self.permissions = Some(permissions);
235 self
236 }
237}
238
239#[async_trait]
240impl Tool for ExitPlanModeTool {
241 fn spec(&self) -> ToolSpec {
242 ToolSpec {
243 name: "exit_plan_mode".into(),
244 description:
245 "Exit plan mode and present your plan for human review. Include a markdown \
246 summary with: (1) your understanding of the current code, (2) the approach \
247 you propose and why, (3) the files you will modify and how, (4) how you will \
248 verify the change is correct. Execution blocks until the reviewer confirms \
249 or rejects the plan."
250 .into(),
251 parameters: json!({
252 "type": "object",
253 "required": ["plan"],
254 "properties": {
255 "plan": {
256 "type": "string",
257 "description": "Markdown summary of your plan."
258 }
259 }
260 }),
261 }
262 }
263
264 async fn execute(&self, arguments: Value) -> Result<String> {
265 let plan_text = arguments
266 .get("plan")
267 .and_then(|v| v.as_str())
268 .unwrap_or("")
269 .to_string();
270
271 if let Some(ref _config) = self.permissions {
273 }
276
277 self.gate
279 .exploring_plan_mode
280 .store(false, Ordering::Relaxed);
281
282 if let Ok(mut w) = self.gate.pending_plan.write() {
284 *w = Some(plan_text.clone());
285 }
286
287 self.event_sink
290 .emit(AgentEvent::PlanProposed {
291 plan_text: plan_text.clone(),
292 tool_calls: vec![],
293 })
294 .await;
295
296 let result = self.gate.wait_for_approval().await;
299
300 match result {
301 PlanApprovalResult::Approved => Ok(json!({ "approved": true }).to_string()),
302 PlanApprovalResult::Rejected { reason } => {
303 Ok(json!({ "approved": false, "reason": reason }).to_string())
304 }
305 }
306 }
307
308 fn side_effect_class(&self) -> ToolSideEffect {
309 ToolSideEffect::External
311 }
312
313 fn is_readonly(&self) -> bool {
314 false
315 }
316}
317
318#[derive(Debug, Clone)]
324pub enum PlanModeRequestResult {
325 Approved,
327 Rejected { reason: String },
329}
330
331pub struct PlanModeRequestGate {
336 response: Arc<RwLock<Option<PlanModeRequestResult>>>,
337 notify: Arc<Notify>,
338}
339
340impl PlanModeRequestGate {
341 pub fn new() -> Self {
343 Self {
344 response: Arc::new(RwLock::new(None)),
345 notify: Arc::new(Notify::new()),
346 }
347 }
348
349 pub async fn wait_for_decision(&self) -> PlanModeRequestResult {
351 loop {
352 let result_opt = {
353 let guard = self
354 .response
355 .read()
356 .expect("PlanModeRequestGate response lock poisoned");
357 guard.clone()
358 };
359 if let Some(result) = result_opt {
360 if let Ok(mut w) = self.response.write() {
361 *w = None;
362 }
363 return result;
364 }
365 self.notify.notified().await;
366 }
367 }
368
369 pub fn approve(&self) {
371 if let Ok(mut w) = self.response.write() {
372 *w = Some(PlanModeRequestResult::Approved);
373 }
374 self.notify.notify_one();
375 }
376
377 pub fn reject(&self, reason: impl Into<String>) {
379 if let Ok(mut w) = self.response.write() {
380 *w = Some(PlanModeRequestResult::Rejected {
381 reason: reason.into(),
382 });
383 }
384 self.notify.notify_one();
385 }
386}
387
388impl Default for PlanModeRequestGate {
389 fn default() -> Self {
390 Self::new()
391 }
392}
393
394pub struct RequestPlanModeTool {
408 gate: Arc<PlanModeRequestGate>,
409 event_sink: Arc<dyn EventSink>,
410}
411
412impl RequestPlanModeTool {
413 pub fn new(gate: Arc<PlanModeRequestGate>, event_sink: Arc<dyn EventSink>) -> Self {
415 Self { gate, event_sink }
416 }
417}
418
419#[async_trait]
420impl Tool for RequestPlanModeTool {
421 fn spec(&self) -> ToolSpec {
422 ToolSpec {
423 name: "request_plan_mode".into(),
424 description:
425 "Before entering plan mode, call this tool to ask the user whether they want \
426 you to create a plan first. Provide a brief `reason` explaining why planning \
427 would be helpful for this task. The call blocks until the user decides. \
428 If approved, proceed with `enter_plan_mode`; if rejected, execute directly \
429 without planning."
430 .into(),
431 parameters: serde_json::json!({
432 "type": "object",
433 "required": ["reason"],
434 "properties": {
435 "reason": {
436 "type": "string",
437 "description": "Brief explanation of why a planning phase would be \
438 beneficial (1-2 sentences)."
439 }
440 }
441 }),
442 }
443 }
444
445 async fn execute(&self, arguments: serde_json::Value) -> Result<String> {
446 let reason = arguments
447 .get("reason")
448 .and_then(|v| v.as_str())
449 .unwrap_or("")
450 .to_string();
451
452 self.event_sink
454 .emit(crate::event::AgentEvent::PlanModeRequested {
455 reason: reason.clone(),
456 })
457 .await;
458
459 let result = self.gate.wait_for_decision().await;
461
462 match result {
463 PlanModeRequestResult::Approved => {
464 self.event_sink
465 .emit(crate::event::AgentEvent::PlanModeApproved)
466 .await;
467 Ok(serde_json::json!({ "approved": true }).to_string())
468 }
469 PlanModeRequestResult::Rejected { reason: r } => {
470 self.event_sink
471 .emit(crate::event::AgentEvent::PlanModeRejected { reason: r.clone() })
472 .await;
473 Ok(serde_json::json!({ "approved": false, "reason": r }).to_string())
474 }
475 }
476 }
477
478 fn side_effect_class(&self) -> ToolSideEffect {
479 ToolSideEffect::External
481 }
482
483 fn is_readonly(&self) -> bool {
484 false
485 }
486}
487
488#[cfg(test)]
493mod tests {
494 use super::*;
495 use crate::event::NullSink;
496
497 fn make_gate() -> Arc<PlanApprovalGate> {
498 Arc::new(PlanApprovalGate::new())
499 }
500
501 #[tokio::test]
504 async fn enter_plan_mode_returns_confirmation_message() {
505 let gate = make_gate();
506 let tool = EnterPlanModeTool::new(gate.clone());
507
508 assert!(
509 !gate.exploring_plan_mode.load(Ordering::Relaxed),
510 "flag should start false"
511 );
512
513 let result = tool.execute(json!({})).await.unwrap();
514 let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
515 assert_eq!(parsed["entered"], true);
516 assert!(
517 gate.exploring_plan_mode.load(Ordering::Relaxed),
518 "flag should be set after enter"
519 );
520 }
521
522 #[tokio::test]
525 async fn exit_plan_mode_blocks_until_confirmed() {
526 let gate = make_gate();
527 gate.exploring_plan_mode.store(true, Ordering::Relaxed);
529
530 let tool = Arc::new(ExitPlanModeTool::new(gate.clone(), Arc::new(NullSink)));
531
532 let gate_clone = gate.clone();
534 let approve_handle = tokio::spawn(async move {
535 tokio::task::yield_now().await;
537 gate_clone.approve();
538 });
539
540 let result = tool
541 .execute(json!({ "plan": "step 1: read; step 2: write" }))
542 .await
543 .unwrap();
544
545 approve_handle.await.unwrap();
546
547 let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
548 assert_eq!(parsed["approved"], true);
549 assert!(
550 !gate.exploring_plan_mode.load(Ordering::Relaxed),
551 "flag should be cleared after exit"
552 );
553 }
554
555 #[tokio::test]
558 async fn exit_plan_mode_returns_rejection_reason() {
559 let gate = make_gate();
560 let tool = Arc::new(ExitPlanModeTool::new(gate.clone(), Arc::new(NullSink)));
561
562 let gate_clone = gate.clone();
563 let reject_handle = tokio::spawn(async move {
564 tokio::task::yield_now().await;
565 gate_clone.reject("Plan is incomplete");
566 });
567
568 let result = tool.execute(json!({ "plan": "my plan" })).await.unwrap();
569
570 reject_handle.await.unwrap();
571
572 let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
573 assert_eq!(parsed["approved"], false);
574 assert_eq!(parsed["reason"], "Plan is incomplete");
575 }
576
577 #[tokio::test]
580 async fn gate_approve_wakes_waiter() {
581 let gate = make_gate();
582 let gate_clone = gate.clone();
583
584 let waiter = tokio::spawn(async move { gate_clone.wait_for_approval().await });
585
586 tokio::task::yield_now().await;
588 gate.approve();
589
590 let result = waiter.await.unwrap();
591 assert!(matches!(result, PlanApprovalResult::Approved));
592 }
593
594 #[tokio::test]
595 async fn gate_reject_wakes_waiter_with_reason() {
596 let gate = make_gate();
597 let gate_clone = gate.clone();
598
599 let waiter = tokio::spawn(async move { gate_clone.wait_for_approval().await });
600
601 tokio::task::yield_now().await;
602 gate.reject("too risky");
603
604 let result = waiter.await.unwrap();
605 assert!(matches!(
606 result,
607 PlanApprovalResult::Rejected { reason } if reason == "too risky"
608 ));
609 }
610
611 #[tokio::test]
614 async fn gate_response_cleared_after_wait() {
615 let gate = make_gate();
616 gate.approve();
617 let _ = gate.wait_for_approval().await;
618
619 let stored = gate.response.read().unwrap().clone();
621 assert!(stored.is_none(), "response should be cleared after read");
622 }
623
624 #[tokio::test]
627 async fn request_gate_approve_wakes_waiter() {
628 let gate = Arc::new(PlanModeRequestGate::new());
629 let gate_clone = gate.clone();
630 let waiter = tokio::spawn(async move { gate_clone.wait_for_decision().await });
631 tokio::task::yield_now().await;
632 gate.approve();
633 let result = waiter.await.unwrap();
634 assert!(matches!(result, PlanModeRequestResult::Approved));
635 }
636
637 #[tokio::test]
638 async fn request_gate_reject_propagates_reason() {
639 let gate = Arc::new(PlanModeRequestGate::new());
640 let gate_clone = gate.clone();
641 let waiter = tokio::spawn(async move { gate_clone.wait_for_decision().await });
642 tokio::task::yield_now().await;
643 gate.reject("user skipped");
644 let result = waiter.await.unwrap();
645 assert!(matches!(
646 result,
647 PlanModeRequestResult::Rejected { reason } if reason == "user skipped"
648 ));
649 }
650
651 #[tokio::test]
652 async fn request_gate_cleared_after_use() {
653 let gate = Arc::new(PlanModeRequestGate::new());
654 gate.approve();
655 let _ = gate.wait_for_decision().await;
656 let stored = gate.response.read().unwrap().clone();
657 assert!(
658 stored.is_none(),
659 "response should be cleared after decision"
660 );
661 }
662
663 #[tokio::test]
664 async fn request_plan_mode_tool_emits_event_and_blocks_until_approved() {
665 use crate::event::NullSink;
666 let gate = Arc::new(PlanModeRequestGate::new());
667 let tool = Arc::new(RequestPlanModeTool::new(gate.clone(), Arc::new(NullSink)));
668 let gate_clone = gate.clone();
669 let approve_handle = tokio::spawn(async move {
670 tokio::task::yield_now().await;
671 gate_clone.approve();
672 });
673 let result = tool
674 .execute(json!({ "reason": "complex task" }))
675 .await
676 .unwrap();
677 approve_handle.await.unwrap();
678 let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
679 assert_eq!(parsed["approved"], true);
680 }
681
682 #[tokio::test]
683 async fn request_plan_mode_tool_rejected_returns_false() {
684 use crate::event::NullSink;
685 let gate = Arc::new(PlanModeRequestGate::new());
686 let tool = Arc::new(RequestPlanModeTool::new(gate.clone(), Arc::new(NullSink)));
687 let gate_clone = gate.clone();
688 let reject_handle = tokio::spawn(async move {
689 tokio::task::yield_now().await;
690 gate_clone.reject("not needed");
691 });
692 let result = tool
693 .execute(json!({ "reason": "might need plan" }))
694 .await
695 .unwrap();
696 reject_handle.await.unwrap();
697 let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
698 assert_eq!(parsed["approved"], false);
699 assert_eq!(parsed["reason"], "not needed");
700 }
701}