Skip to main content

apollo/agent/
mode.rs

1//! Agent execution modes — coding mode, swarm mode.
2
3use std::path::PathBuf;
4
5use async_trait::async_trait;
6use tokio::sync::mpsc;
7
8use crate::channels::{Channel, IncomingMessage, OutgoingMessage};
9
10/// Agent execution mode.
11#[derive(Debug, Clone, Default)]
12pub enum AgentMode {
13    /// Auto: heuristic-based classification (default).
14    ///
15    /// Short conversational messages skip planning. Longer or tool-requiring
16    /// messages go through the Planning → Executing → Summarizing state machine.
17    #[default]
18    Auto,
19
20    /// Bypass all permission checks and approval steps — agent runs fully autonomously.
21    ///
22    /// Equivalent to Claude Code's `--dangerously-skip-permissions`. The agent:
23    /// - Never pauses for plan approval
24    /// - Executes all tool calls without confirmation
25    /// - Defaults to the heavy model (Opus) for maximum capability
26    /// - Always plans before executing
27    ///
28    /// Only enable this in trusted, sandboxed environments or when you explicitly
29    /// want autonomous unattended execution.
30    BypassPermissions,
31
32    /// Coding mode: optimized for software development.
33    ///
34    /// - Always plans before executing
35    /// - Prefers the heavy model (Opus) for execution
36    /// - Injects coding-specific rules into the system prompt
37    /// - When `plan_approval` is enabled, previews the plan to the user
38    ///   and waits for explicit confirmation before executing
39    Coding {
40        plan_approval: bool,
41        project_path: Option<PathBuf>,
42    },
43
44    /// Swarm mode: deploy parallel coding agents.
45    ///
46    /// The planner decomposes the task, then spawns up to `parallelism`
47    /// concurrent AgentRunner workers. Results are merged into a summary.
48    Swarm { parallelism: usize },
49}
50
51/// Maps stored `agent.permission_profile` to the initial runtime mode.
52pub fn agent_mode_from_permission_profile(profile: &str) -> AgentMode {
53    let p = profile.trim().to_ascii_lowercase().replace(['-', ' '], "_");
54    match p.as_str() {
55        "full" => AgentMode::BypassPermissions,
56        "prompt" => AgentMode::Coding {
57            plan_approval: true,
58            project_path: None,
59        },
60        _ => AgentMode::Auto,
61    }
62}
63
64impl AgentMode {
65    pub fn is_coding(&self) -> bool {
66        matches!(self, Self::Coding { .. })
67    }
68
69    pub fn is_swarm(&self) -> bool {
70        matches!(self, Self::Swarm { .. })
71    }
72
73    /// Whether all permission checks and approval steps should be skipped.
74    /// True for `BypassPermissions` and `Coding { plan_approval: false }`.
75    pub fn bypass_permissions(&self) -> bool {
76        matches!(self, Self::BypassPermissions)
77    }
78
79    /// Whether to show a plan preview and wait for user approval before executing.
80    /// Always false in `BypassPermissions` mode even if `plan_approval` was set.
81    pub fn plan_approval(&self) -> bool {
82        if self.bypass_permissions() {
83            return false;
84        }
85        matches!(
86            self,
87            Self::Coding {
88                plan_approval: true,
89                ..
90            }
91        )
92    }
93
94    pub fn swarm_parallelism(&self) -> usize {
95        match self {
96            Self::Swarm { parallelism } => *parallelism,
97            _ => 1,
98        }
99    }
100
101    /// System prompt section to inject when this mode is active.
102    pub fn system_prompt_injection(&self) -> Option<&'static str> {
103        match self {
104            Self::BypassPermissions => Some(BYPASS_PERMISSIONS_PROMPT),
105            Self::Coding { .. } => Some(CODING_MODE_PROMPT),
106            Self::Swarm { .. } => Some(SWARM_MODE_PROMPT),
107            Self::Auto => None,
108        }
109    }
110
111    /// Whether to prefer the heavy model for execution.
112    pub fn prefer_heavy_model(&self) -> bool {
113        matches!(
114            self,
115            Self::BypassPermissions | Self::Coding { .. } | Self::Swarm { .. }
116        )
117    }
118
119    /// Whether to always plan (skip the conversational shortcut).
120    pub fn always_plan(&self) -> bool {
121        matches!(
122            self,
123            Self::BypassPermissions | Self::Coding { .. } | Self::Swarm { .. }
124        )
125    }
126}
127
128/// No-op channel for headless swarm worker agents.
129/// All operations are discarded — only the return value of `handle_message` is used.
130pub struct NullChannel {
131    name: String,
132}
133
134impl NullChannel {
135    pub fn new(name: impl Into<String>) -> Self {
136        Self { name: name.into() }
137    }
138}
139
140#[async_trait]
141impl Channel for NullChannel {
142    fn name(&self) -> &str {
143        &self.name
144    }
145
146    async fn start(&mut self) -> anyhow::Result<mpsc::Receiver<IncomingMessage>> {
147        let (_tx, rx) = mpsc::channel(1);
148        Ok(rx) // tx dropped immediately — receiver never yields messages
149    }
150
151    async fn send(&self, _message: OutgoingMessage) -> anyhow::Result<Option<String>> {
152        Ok(None)
153    }
154
155    async fn stop(&mut self) -> anyhow::Result<()> {
156        Ok(())
157    }
158}
159
160const BYPASS_PERMISSIONS_PROMPT: &str = "\
161## Autonomous Mode (Bypass Permissions)
162
163You are running with all permission checks disabled. You have full autonomy to:
164- Execute shell commands without confirmation
165- Read and write any files in the workspace
166- Make and push commits autonomously
167- Install dependencies and run builds
168
169Operate decisively. Do not ask for permission before taking actions. \
170Complete the task end-to-end and report what was done.";
171
172const CODING_MODE_PROMPT: &str = "\
173## Coding Mode Active
174
175You are in coding mode. Follow these rules:
176- Read files before editing them
177- Make surgical, minimal changes — don't reformat code you don't touch
178- After edits, verify correctness (cargo build, npm test, or equivalent)
179- Prefer targeted `edit` operations over full file rewrites
180- For multi-file changes: enumerate all affected files first, then execute in order
181- Diagnose root causes before retrying failures
182- Commit at natural checkpoints with clear, descriptive messages
183- Never break working code — test incrementally";
184
185const SWARM_MODE_PROMPT: &str = "\
186## Swarm Deployment Mode Active
187
188You are coordinating parallel coding agents. Follow these rules:
189- Decompose the goal into independent, non-overlapping subtasks
190- Assign each agent a clear, bounded scope (a file, module, or feature)
191- Avoid shared-state conflicts between agents
192- Validate and integrate each agent's output before merging
193- Reassign or handle failed tasks yourself
194- Report the outcome of each agent with status and a brief summary";
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn test_agent_mode_from_permission_profile() {
202        assert!(matches!(
203            agent_mode_from_permission_profile("full"),
204            AgentMode::BypassPermissions
205        ));
206        assert!(matches!(
207            agent_mode_from_permission_profile("  FULL  "),
208            AgentMode::BypassPermissions
209        ));
210        assert!(matches!(
211            agent_mode_from_permission_profile("prompt"),
212            AgentMode::Coding {
213                plan_approval: true,
214                project_path: None
215            }
216        ));
217        assert!(matches!(
218            agent_mode_from_permission_profile(" ProMpT\n"),
219            AgentMode::Coding {
220                plan_approval: true,
221                project_path: None
222            }
223        ));
224        assert!(matches!(
225            agent_mode_from_permission_profile(""),
226            AgentMode::Auto
227        ));
228        assert!(matches!(
229            agent_mode_from_permission_profile("unknown-profile"),
230            AgentMode::Auto
231        ));
232    }
233}