bamboo_tools/tools/
enter_plan_mode.rs1use async_trait::async_trait;
2use bamboo_agent_core::{Tool, ToolError, ToolResult};
3use serde::Deserialize;
4use serde_json::json;
5
6#[derive(Debug, Deserialize)]
7struct EnterPlanModeArgs {
8 #[serde(default)]
9 reason: Option<String>,
10}
11
12pub struct EnterPlanModeTool;
13
14impl EnterPlanModeTool {
15 pub fn new() -> Self {
16 Self
17 }
18}
19
20impl Default for EnterPlanModeTool {
21 fn default() -> Self {
22 Self::new()
23 }
24}
25
26#[async_trait]
27impl Tool for EnterPlanModeTool {
28 fn name(&self) -> &str {
29 "EnterPlanMode"
30 }
31
32 fn description(&self) -> &str {
33 "Switch to plan mode for complex tasks requiring exploration and design before implementation"
34 }
35
36 fn parameters_schema(&self) -> serde_json::Value {
37 json!({
38 "type": "object",
39 "properties": {
40 "reason": {
41 "type": "string",
42 "description": "Optional reason for entering plan mode"
43 }
44 },
45 "additionalProperties": false
46 })
47 }
48
49 async fn execute(&self, args: serde_json::Value) -> Result<ToolResult, ToolError> {
50 let parsed: EnterPlanModeArgs = serde_json::from_value(args).map_err(|e| {
51 ToolError::InvalidArguments(format!("Invalid EnterPlanMode args: {}", e))
52 })?;
53
54 let question = if let Some(ref reason) = parsed.reason {
55 format!(
56 "Enter plan mode? The assistant wants to switch to read-only exploration to design an approach before any changes. Reason: {}",
57 reason
58 )
59 } else {
60 "Enter plan mode? The assistant will switch to read-only exploration to design an approach before any changes.".to_string()
61 };
62
63 let payload = json!({
64 "status": "awaiting_user_input",
65 "question": question,
66 "options": ["Enter plan mode", "Stay in normal mode"],
67 "allow_custom": false,
68 });
69
70 Ok(ToolResult {
71 success: true,
72 result: payload.to_string(),
73 display_preference: Some("conclusion_with_options".to_string()),
74 })
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use serde_json::json;
82
83 #[test]
84 fn enter_plan_mode_has_correct_name() {
85 let tool = EnterPlanModeTool::new();
86 assert_eq!(tool.name(), "EnterPlanMode");
87 }
88
89 #[test]
90 fn enter_plan_mode_has_description() {
91 let tool = EnterPlanModeTool::new();
92 assert!(!tool.description().is_empty());
93 assert!(tool.description().contains("plan"));
94 }
95
96 #[tokio::test]
97 async fn enter_plan_mode_returns_conclusion_with_options() {
98 let tool = EnterPlanModeTool::new();
99 let result = tool.execute(json!({})).await.unwrap();
100
101 assert!(result.success);
102 assert_eq!(
103 result.display_preference,
104 Some("conclusion_with_options".to_string())
105 );
106
107 let payload: serde_json::Value = serde_json::from_str(&result.result).unwrap();
108 assert_eq!(payload["status"], "awaiting_user_input");
109 assert!(payload["question"]
110 .as_str()
111 .unwrap()
112 .contains("Enter plan mode"));
113 let options = payload["options"].as_array().unwrap();
114 assert_eq!(options.len(), 2);
115 assert!(options.contains(&json!("Enter plan mode")));
116 assert!(options.contains(&json!("Stay in normal mode")));
117 assert_eq!(payload["allow_custom"], false);
118 }
119
120 #[tokio::test]
121 async fn enter_plan_mode_includes_reason() {
122 let tool = EnterPlanModeTool::new();
123 let result = tool
124 .execute(json!({
125 "reason": "This is a complex refactor"
126 }))
127 .await
128 .unwrap();
129
130 assert!(result.success);
131 let payload: serde_json::Value = serde_json::from_str(&result.result).unwrap();
132 let question = payload["question"].as_str().unwrap();
133 assert!(question.contains("This is a complex refactor"));
134 }
135
136 #[tokio::test]
137 async fn enter_plan_mode_accepts_empty_args() {
138 let tool = EnterPlanModeTool::new();
139 let result = tool.execute(json!({})).await.unwrap();
140 assert!(result.success);
141 }
142}