Skip to main content

apollo/tools/
mode_switch.rs

1//! ModeSwitchTool — let the agent switch its own execution mode at runtime.
2//!
3//! Useful in multi-phase workflows: start in Auto, switch to Coding for
4//! implementation work, then back to Auto for conversation.
5
6use std::path::PathBuf;
7use std::sync::{Arc, RwLock};
8
9use async_trait::async_trait;
10use serde::Deserialize;
11
12use super::traits::*;
13use crate::agent::mode::AgentMode;
14
15pub struct ModeSwitchTool {
16    mode: Arc<RwLock<AgentMode>>,
17}
18
19impl ModeSwitchTool {
20    pub fn new(mode: Arc<RwLock<AgentMode>>) -> Self {
21        Self { mode }
22    }
23}
24
25#[derive(Deserialize)]
26struct ModeSwitchArgs {
27    /// Target mode: "auto", "coding", "bypass", "swarm"
28    mode: String,
29    /// For coding mode: require user approval before executing the plan
30    #[serde(default)]
31    plan_approval: bool,
32    /// For coding mode: project path to scope file operations
33    #[serde(default)]
34    project_path: Option<String>,
35    /// For swarm mode: max parallel agents (default 3)
36    #[serde(default = "default_parallelism")]
37    parallelism: usize,
38}
39
40fn default_parallelism() -> usize {
41    3
42}
43
44#[async_trait]
45impl Tool for ModeSwitchTool {
46    fn name(&self) -> &str {
47        "mode_switch"
48    }
49
50    fn spec(&self) -> ToolSpec {
51        ToolSpec {
52            name: "mode_switch".to_string(),
53            description: "Switch the agent's execution mode at runtime. \
54                'auto' = default heuristic mode. \
55                'coding' = optimized for software development (always plans, prefers Opus). \
56                'bypass' = fully autonomous, skips all approval steps. \
57                'swarm' = deploy parallel agents."
58                .to_string(),
59            parameters: serde_json::json!({
60                "type": "object",
61                "properties": {
62                    "mode": {
63                        "type": "string",
64                        "enum": ["auto", "coding", "bypass", "swarm"],
65                        "description": "Target execution mode"
66                    },
67                    "plan_approval": {
68                        "type": "boolean",
69                        "description": "Coding mode only: show plan and wait for approval before executing (default false)"
70                    },
71                    "project_path": {
72                        "type": "string",
73                        "description": "Coding mode only: project root path"
74                    },
75                    "parallelism": {
76                        "type": "integer",
77                        "description": "Swarm mode only: max concurrent agents (default 3)",
78                        "minimum": 1,
79                        "maximum": 10
80                    }
81                },
82                "required": ["mode"]
83            }),
84        }
85    }
86
87    async fn execute(&self, arguments: &str) -> anyhow::Result<ToolResult> {
88        let args: ModeSwitchArgs = serde_json::from_str(arguments)?;
89
90        let new_mode = match args.mode.as_str() {
91            "auto" => AgentMode::Auto,
92            "coding" => AgentMode::Coding {
93                plan_approval: args.plan_approval,
94                project_path: args.project_path.map(PathBuf::from),
95            },
96            "bypass" => AgentMode::BypassPermissions,
97            "swarm" => AgentMode::Swarm {
98                parallelism: args.parallelism.clamp(1, 10),
99            },
100            other => return Ok(ToolResult::error(format!("Unknown mode: {}", other))),
101        };
102
103        let description = match &new_mode {
104            AgentMode::Auto => "Auto (heuristic mode)".to_string(),
105            AgentMode::BypassPermissions => "BypassPermissions (autonomous mode)".to_string(),
106            AgentMode::Coding { plan_approval, .. } => {
107                format!("Coding (plan_approval={})", plan_approval)
108            }
109            AgentMode::Swarm { parallelism } => {
110                format!("Swarm (parallelism={})", parallelism)
111            }
112        };
113
114        *self.mode.write().unwrap() = new_mode;
115        Ok(ToolResult::success(format!(
116            "Switched to {} mode",
117            description
118        )))
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[tokio::test]
127    async fn switch_to_coding() {
128        let mode = Arc::new(RwLock::new(AgentMode::Auto));
129        let tool = ModeSwitchTool::new(mode.clone());
130
131        let r = tool
132            .execute(r#"{"mode":"coding","plan_approval":true}"#)
133            .await
134            .unwrap();
135        assert!(!r.is_error);
136        assert!(r.output.contains("Coding"));
137
138        assert!(mode.read().unwrap().is_coding());
139    }
140
141    #[tokio::test]
142    async fn switch_to_bypass() {
143        let mode = Arc::new(RwLock::new(AgentMode::Auto));
144        let tool = ModeSwitchTool::new(mode.clone());
145
146        tool.execute(r#"{"mode":"bypass"}"#).await.unwrap();
147        assert!(mode.read().unwrap().bypass_permissions());
148    }
149
150    #[tokio::test]
151    async fn switch_back_to_auto() {
152        let mode = Arc::new(RwLock::new(AgentMode::BypassPermissions));
153        let tool = ModeSwitchTool::new(mode.clone());
154
155        tool.execute(r#"{"mode":"auto"}"#).await.unwrap();
156        assert!(matches!(*mode.read().unwrap(), AgentMode::Auto));
157    }
158}