1use std::collections::HashMap;
4use std::path::PathBuf;
5use std::sync::{Arc, Mutex};
6use std::time::{Duration, Instant};
7
8use async_trait::async_trait;
9use tokio::sync::mpsc;
10
11use crate::channels::{Channel, IncomingMessage, OutgoingMessage};
12
13#[derive(Debug, Clone, Default)]
15pub enum AgentMode {
16 #[default]
21 Auto,
22
23 BypassPermissions,
34
35 Coding {
43 plan_approval: bool,
44 project_path: Option<PathBuf>,
45 },
46
47 Swarm { parallelism: usize },
52}
53
54pub fn agent_mode_from_permission_profile(profile: &str) -> AgentMode {
56 let p = profile.trim().to_ascii_lowercase().replace(['-', ' '], "_");
57 match p.as_str() {
58 "full" => AgentMode::BypassPermissions,
59 "prompt" => AgentMode::Coding {
60 plan_approval: true,
61 project_path: None,
62 },
63 _ => AgentMode::Auto,
64 }
65}
66
67impl AgentMode {
68 pub fn is_coding(&self) -> bool {
69 matches!(self, Self::Coding { .. })
70 }
71
72 pub fn is_swarm(&self) -> bool {
73 matches!(self, Self::Swarm { .. })
74 }
75
76 pub fn bypass_permissions(&self) -> bool {
79 matches!(self, Self::BypassPermissions)
80 }
81
82 pub fn plan_approval(&self) -> bool {
85 if self.bypass_permissions() {
86 return false;
87 }
88 matches!(
89 self,
90 Self::Coding {
91 plan_approval: true,
92 ..
93 }
94 )
95 }
96
97 pub fn swarm_parallelism(&self) -> usize {
98 match self {
99 Self::Swarm { parallelism } => *parallelism,
100 _ => 1,
101 }
102 }
103
104 pub fn system_prompt_injection(&self) -> Option<&'static str> {
106 match self {
107 Self::BypassPermissions => Some(BYPASS_PERMISSIONS_PROMPT),
108 Self::Coding { .. } => Some(CODING_MODE_PROMPT),
109 Self::Swarm { .. } => Some(SWARM_MODE_PROMPT),
110 Self::Auto => None,
111 }
112 }
113
114 pub fn prefer_heavy_model(&self) -> bool {
116 matches!(
117 self,
118 Self::BypassPermissions | Self::Coding { .. } | Self::Swarm { .. }
119 )
120 }
121
122 pub fn always_plan(&self) -> bool {
124 matches!(
125 self,
126 Self::BypassPermissions | Self::Coding { .. } | Self::Swarm { .. }
127 )
128 }
129}
130
131#[derive(Debug, Clone)]
133pub struct PendingPlan {
134 pub plan: String,
135 pub original_message: String,
136 pub preferred_model: String,
137 pub created_at: Instant,
138}
139
140impl PendingPlan {
141 const TTL: Duration = Duration::from_secs(30 * 60); pub fn is_expired(&self) -> bool {
144 self.created_at.elapsed() > Self::TTL
145 }
146}
147
148pub type PendingPlans = Arc<Mutex<HashMap<String, PendingPlan>>>;
149
150pub fn is_approval(text: &str) -> bool {
152 let lower = text.trim().to_lowercase();
153 matches!(
154 lower.as_str(),
155 "yes"
156 | "y"
157 | "go"
158 | "ok"
159 | "okay"
160 | "proceed"
161 | "approve"
162 | "do it"
163 | "looks good"
164 | "lgtm"
165 | "go ahead"
166 | "continue"
167 | "run it"
168 | "execute"
169 ) || lower.starts_with("yes,")
170 || lower.starts_with("go ahead")
171 || lower.starts_with("looks good")
172 || lower.starts_with("lgtm")
173}
174
175pub fn is_rejection(text: &str) -> bool {
177 let lower = text.trim().to_lowercase();
178 matches!(
179 lower.as_str(),
180 "no" | "n" | "nope" | "cancel" | "abort" | "stop" | "nevermind" | "never mind"
181 ) || lower.starts_with("no,")
182 || lower.starts_with("cancel")
183 || lower.starts_with("stop ")
184}
185
186pub struct NullChannel {
189 name: String,
190}
191
192impl NullChannel {
193 pub fn new(name: impl Into<String>) -> Self {
194 Self { name: name.into() }
195 }
196}
197
198#[async_trait]
199impl Channel for NullChannel {
200 fn name(&self) -> &str {
201 &self.name
202 }
203
204 async fn start(&mut self) -> anyhow::Result<mpsc::Receiver<IncomingMessage>> {
205 let (_tx, rx) = mpsc::channel(1);
206 Ok(rx) }
208
209 async fn send(&self, _message: OutgoingMessage) -> anyhow::Result<Option<String>> {
210 Ok(None)
211 }
212
213 async fn stop(&mut self) -> anyhow::Result<()> {
214 Ok(())
215 }
216}
217
218const BYPASS_PERMISSIONS_PROMPT: &str = "\
219## Autonomous Mode (Bypass Permissions)
220
221You are running with all permission checks disabled. You have full autonomy to:
222- Execute shell commands without confirmation
223- Read and write any files in the workspace
224- Make and push commits autonomously
225- Install dependencies and run builds
226
227Operate decisively. Do not ask for permission before taking actions. \
228Complete the task end-to-end and report what was done.";
229
230const CODING_MODE_PROMPT: &str = "\
231## Coding Mode Active
232
233You are in coding mode. Follow these rules:
234- Read files before editing them
235- Make surgical, minimal changes — don't reformat code you don't touch
236- After edits, verify correctness (cargo build, npm test, or equivalent)
237- Prefer targeted `edit` operations over full file rewrites
238- For multi-file changes: enumerate all affected files first, then execute in order
239- Diagnose root causes before retrying failures
240- Commit at natural checkpoints with clear, descriptive messages
241- Never break working code — test incrementally";
242
243const SWARM_MODE_PROMPT: &str = "\
244## Swarm Deployment Mode Active
245
246You are coordinating parallel coding agents. Follow these rules:
247- Decompose the goal into independent, non-overlapping subtasks
248- Assign each agent a clear, bounded scope (a file, module, or feature)
249- Avoid shared-state conflicts between agents
250- Validate and integrate each agent's output before merging
251- Reassign or handle failed tasks yourself
252- Report the outcome of each agent with status and a brief summary";
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn test_agent_mode_from_permission_profile() {
260 assert!(matches!(
261 agent_mode_from_permission_profile("full"),
262 AgentMode::BypassPermissions
263 ));
264 assert!(matches!(
265 agent_mode_from_permission_profile(" FULL "),
266 AgentMode::BypassPermissions
267 ));
268 assert!(matches!(
269 agent_mode_from_permission_profile("prompt"),
270 AgentMode::Coding {
271 plan_approval: true,
272 project_path: None
273 }
274 ));
275 assert!(matches!(
276 agent_mode_from_permission_profile(" ProMpT\n"),
277 AgentMode::Coding {
278 plan_approval: true,
279 project_path: None
280 }
281 ));
282 assert!(matches!(
283 agent_mode_from_permission_profile(""),
284 AgentMode::Auto
285 ));
286 assert!(matches!(
287 agent_mode_from_permission_profile("unknown-profile"),
288 AgentMode::Auto
289 ));
290 }
291
292 #[test]
293 fn test_is_approval() {
294 assert!(is_approval("yes"));
296 assert!(is_approval("y"));
297 assert!(is_approval("go"));
298 assert!(is_approval("ok"));
299 assert!(is_approval("okay"));
300 assert!(is_approval("proceed"));
301 assert!(is_approval("approve"));
302 assert!(is_approval("do it"));
303 assert!(is_approval("looks good"));
304 assert!(is_approval("lgtm"));
305 assert!(is_approval("go ahead"));
306 assert!(is_approval("continue"));
307 assert!(is_approval("run it"));
308 assert!(is_approval("execute"));
309
310 assert!(is_approval("YES"));
312 assert!(is_approval("OkAy"));
313 assert!(is_approval("PROCEED"));
314 assert!(is_approval("LGTM"));
315
316 assert!(is_approval(" yes "));
318 assert!(is_approval("\tgo ahead\n"));
319
320 assert!(is_approval("yes, do it"));
322 assert!(is_approval("go ahead and run"));
323 assert!(is_approval("looks good to me"));
324 assert!(is_approval("lgtm, go for it"));
325
326 assert!(!is_approval("no"));
328 assert!(!is_approval("yep"));
329 assert!(!is_approval("sure"));
330 assert!(!is_approval(""));
331 assert!(!is_approval(" "));
332 assert!(!is_approval("go ahea")); }
334
335 #[test]
336 fn test_is_rejection() {
337 assert!(is_rejection("no"));
339 assert!(is_rejection("n"));
340 assert!(is_rejection("nope"));
341 assert!(is_rejection("cancel"));
342 assert!(is_rejection("abort"));
343 assert!(is_rejection("stop"));
344 assert!(is_rejection("nevermind"));
345 assert!(is_rejection("never mind"));
346
347 assert!(is_rejection("NO"));
349 assert!(is_rejection("Cancel"));
350 assert!(is_rejection("ABORT"));
351 assert!(is_rejection("STOP"));
352
353 assert!(is_rejection(" no "));
355 assert!(is_rejection("\tabort\n"));
356
357 assert!(is_rejection("no, don't do that"));
359 assert!(is_rejection("cancel the operation"));
360 assert!(is_rejection("stop executing"));
361
362 assert!(!is_rejection("yes"));
364 assert!(!is_rejection("not really"));
365 assert!(!is_rejection(""));
366 assert!(!is_rejection(" "));
367 assert!(!is_rejection("stopit")); }
369}