j_agent/context/
plan_state.rs1use crate::message_types::PlanDecision;
10use crate::tools::tool_names;
11use std::collections::VecDeque;
12use std::sync::{Arc, Condvar, Mutex};
13
14const PLAN_APPROVAL_TIMEOUT_SECS: u64 = 120;
16
17pub struct PendingPlanApproval {
22 pub agent_name: String,
24 pub plan_content: String,
26 pub plan_name: String,
28 decision: Arc<(Mutex<Option<PlanDecision>>, Condvar)>,
30}
31
32impl PendingPlanApproval {
33 pub fn new(agent_name: String, plan_content: String, plan_name: String) -> Arc<Self> {
35 Arc::new(Self {
36 agent_name,
37 plan_content,
38 plan_name,
39 decision: Arc::new((Mutex::new(None), Condvar::new())),
40 })
41 }
42
43 pub fn wait_for_decision(&self, timeout_secs: u64) -> PlanDecision {
45 let (lock, cvar) = &*self.decision;
46 let guard = lock.lock().unwrap_or_else(|e| e.into_inner());
47 let (mut guard, _timed_out) = cvar
48 .wait_timeout_while(guard, std::time::Duration::from_secs(timeout_secs), |d| {
49 d.is_none()
50 })
51 .unwrap_or_else(|e| e.into_inner());
52 if guard.is_none() {
53 *guard = Some(PlanDecision::Reject);
54 }
55 guard.clone().unwrap_or(PlanDecision::Reject)
56 }
57
58 pub fn resolve(&self, decision: PlanDecision) {
60 let (lock, cvar) = &*self.decision;
61 let mut d = lock.lock().unwrap_or_else(|e| e.into_inner());
62 *d = Some(decision);
63 cvar.notify_one();
64 }
65}
66
67pub struct PlanApprovalQueue {
69 pending: Mutex<VecDeque<Arc<PendingPlanApproval>>>,
70}
71
72impl Default for PlanApprovalQueue {
73 fn default() -> Self {
74 Self::new()
75 }
76}
77
78impl PlanApprovalQueue {
79 pub fn new() -> Self {
81 Self {
82 pending: Mutex::new(VecDeque::new()),
83 }
84 }
85
86 pub fn request_blocking(&self, req: Arc<PendingPlanApproval>) -> PlanDecision {
89 {
90 let mut q = self.pending.lock().unwrap_or_else(|e| e.into_inner());
91 q.push_back(Arc::clone(&req));
92 }
93 req.wait_for_decision(PLAN_APPROVAL_TIMEOUT_SECS)
94 }
95
96 pub fn pop_pending(&self) -> Option<Arc<PendingPlanApproval>> {
98 self.pending
99 .lock()
100 .unwrap_or_else(|e| e.into_inner())
101 .pop_front()
102 }
103
104 pub fn deny_all(&self) {
106 let mut q = self.pending.lock().unwrap_or_else(|e| e.into_inner());
107 for req in q.drain(..) {
108 req.resolve(PlanDecision::Reject);
109 }
110 }
111}
112
113#[derive(Debug)]
117struct PlanModeInner {
118 active: bool,
119 plan_file_path: Option<String>,
120}
121
122#[derive(Debug)]
129pub struct PlanModeState {
130 inner: Mutex<PlanModeInner>,
131}
132
133impl Default for PlanModeState {
134 fn default() -> Self {
135 Self::new()
136 }
137}
138
139impl PlanModeState {
140 pub fn new() -> Self {
141 Self {
142 inner: Mutex::new(PlanModeInner {
143 active: false,
144 plan_file_path: None,
145 }),
146 }
147 }
148
149 pub fn is_active(&self) -> bool {
151 self.inner.lock().map(|g| g.active).unwrap_or(false)
152 }
153
154 pub fn enter(&self, path: impl Into<String>) -> Result<(), String> {
157 let path = path.into();
158 match self.inner.lock() {
159 Ok(mut guard) => {
160 if guard.active {
161 return Err("Already in plan mode. Use ExitPlanMode to exit.".to_string());
162 }
163 guard.active = true;
164 guard.plan_file_path = Some(path);
165 Ok(())
166 }
167 Err(e) => Err(format!("Lock poisoned: {}", e)),
168 }
169 }
170
171 pub fn exit(&self) {
173 if let Ok(mut guard) = self.inner.lock() {
174 guard.active = false;
175 guard.plan_file_path = None;
176 }
177 }
178
179 pub fn get_state(&self) -> (bool, Option<String>) {
182 match self.inner.lock() {
183 Ok(guard) => (guard.active, guard.plan_file_path.clone()),
184 Err(_) => (false, None),
185 }
186 }
187
188 pub fn get_plan_file_path(&self) -> Option<String> {
190 self.inner.lock().ok()?.plan_file_path.clone()
191 }
192}
193
194pub const PLAN_MODE_WHITELIST: &[&str] = &[
198 tool_names::READ,
199 tool_names::GLOB,
200 tool_names::GREP,
201 tool_names::WEB_FETCH,
202 tool_names::WEB_SEARCH,
203 tool_names::ASK,
204 tool_names::COMPACT,
205 tool_names::TODO_READ,
206 tool_names::TODO_WRITE,
207 tool_names::TASK_OUTPUT,
208 tool_names::TASK,
209 tool_names::ENTER_PLAN_MODE,
210 tool_names::EXIT_PLAN_MODE,
211 tool_names::ENTER_WORKTREE,
212 tool_names::EXIT_WORKTREE,
213 tool_names::AGENT, tool_names::TEAMMATE, ];
216
217pub fn is_allowed_in_plan_mode(tool_name: &str) -> bool {
219 PLAN_MODE_WHITELIST.contains(&tool_name)
220}