Skip to main content

j_agent/context/
plan_state.rs

1//! Plan Mode 状态管理与审批队列
2//!
3//! 包含:
4//! - `PlanModeState`: Plan mode 全局状态(active + plan_file_path,受 Mutex 保护)
5//! - `PlanApprovalQueue`: Teammate Plan 审批请求队列
6//! - `PendingPlanApproval`: 单条审批请求(Condvar 阻塞等待)
7//! - `PLAN_MODE_WHITELIST`: Plan mode 允许的工具白名单
8
9use crate::message_types::PlanDecision;
10use crate::tools::tool_names;
11use std::collections::VecDeque;
12use std::sync::{Arc, Condvar, Mutex};
13
14/// Plan 审批等待超时(秒)
15const PLAN_APPROVAL_TIMEOUT_SECS: u64 = 120;
16
17// ========== Plan Approval Queue (Teammate → TUI) ==========
18
19// NOTE: Cannot derive Debug - contains Condvar which does not implement Debug
20/// 单条待决 Plan 审批请求(共享给 TUI 和 teammate 线程)
21pub struct PendingPlanApproval {
22    /// 发起请求的 agent 名称("Frontend"/"Backend" 等)
23    pub agent_name: String,
24    /// Plan 文件内容
25    pub plan_content: String,
26    /// Plan 名称(plan-xxx.md 的 xxx)
27    pub plan_name: String,
28    /// 决策通知(None=未决, Some(PlanDecision)=已决)
29    decision: Arc<(Mutex<Option<PlanDecision>>, Condvar)>,
30}
31
32impl PendingPlanApproval {
33    /// 创建一条待决的 Plan 审批请求,返回 Arc 包装实例
34    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    /// Teammate 线程调用:阻塞等待决策,超时返回 Reject
44    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    /// TUI 线程调用:设置决策并唤醒等待的 teammate 线程
59    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
67/// Plan 审批请求队列(主 TUI 和所有 teammate 线程共享同一个 Arc 实例)
68pub 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    /// 创建新的 Plan 审批队列
80    pub fn new() -> Self {
81        Self {
82            pending: Mutex::new(VecDeque::new()),
83        }
84    }
85
86    /// Teammate 线程调用:把请求加入队列并阻塞等待
87    /// 返回 PlanDecision 表示用户决策
88    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    /// TUI 循环调用:取出下一个待决请求(非阻塞)
97    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    /// Session 取消时调用:拒绝所有挂起的请求,唤醒所有等待线程
105    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// ========== Plan Mode State ==========
114
115/// Plan mode 内部状态(受 Mutex 保护,保证原子性)
116#[derive(Debug)]
117struct PlanModeInner {
118    active: bool,
119    plan_file_path: Option<String>,
120}
121
122/// Plan Mode 全局状态(跨工具共享)
123///
124/// 使用单一 Mutex 保护 active + plan_file_path,避免以下并发问题:
125/// - enter() 的 TOCTOU 竞态(先检查 is_active 再进入)
126/// - exit() 不清理 plan_file_path
127/// - is_active() 与 get_plan_file_path() 之间状态不一致
128#[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    /// 检查是否处于 plan mode
150    pub fn is_active(&self) -> bool {
151        self.inner.lock().map(|g| g.active).unwrap_or(false)
152    }
153
154    /// 进入 plan mode,同时设置 plan 文件路径
155    /// 返回 Ok(()) 表示成功进入,Err(msg) 表示已在 plan mode
156    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    /// 退出 plan mode,保留 plan 文件(不删除)
172    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    /// 原子地检查是否 active 并获取 plan 文件路径
180    /// 返回 (is_active, plan_file_path)
181    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    /// 获取 plan 文件路径(仅在 active 时有意义)
189    pub fn get_plan_file_path(&self) -> Option<String> {
190        self.inner.lock().ok()?.plan_file_path.clone()
191    }
192}
193
194/// plan mode 下允许执行的工具白名单
195///
196/// 所有名称均引用 `tool_names` 常量,与各工具的 `NAME` 定义自动对齐。
197pub 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,    // plan mode 允许启动子 agent 做代码探索
214    tool_names::TEAMMATE, // plan mode 允许创建 teammate
215];
216
217/// 检查工具是否在 plan mode 白名单中
218pub fn is_allowed_in_plan_mode(tool_name: &str) -> bool {
219    PLAN_MODE_WHITELIST.contains(&tool_name)
220}