apollo/tools/
mode_switch.rs1use 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 mode: String,
29 #[serde(default)]
31 plan_approval: bool,
32 #[serde(default)]
34 project_path: Option<String>,
35 #[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}