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 images: Vec::new(),
75 })
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use serde_json::json;
83
84 #[test]
85 fn enter_plan_mode_has_correct_name() {
86 let tool = EnterPlanModeTool::new();
87 assert_eq!(tool.name(), "EnterPlanMode");
88 }
89
90 #[test]
91 fn enter_plan_mode_has_description() {
92 let tool = EnterPlanModeTool::new();
93 assert!(!tool.description().is_empty());
94 assert!(tool.description().contains("plan"));
95 }
96
97 #[tokio::test]
98 async fn enter_plan_mode_returns_conclusion_with_options() {
99 let tool = EnterPlanModeTool::new();
100 let result = tool.execute(json!({})).await.unwrap();
101
102 assert!(result.success);
103 assert_eq!(
104 result.display_preference,
105 Some("conclusion_with_options".to_string())
106 );
107
108 let payload: serde_json::Value = serde_json::from_str(&result.result).unwrap();
109 assert_eq!(payload["status"], "awaiting_user_input");
110 assert!(payload["question"]
111 .as_str()
112 .unwrap()
113 .contains("Enter plan mode"));
114 let options = payload["options"].as_array().unwrap();
115 assert_eq!(options.len(), 2);
116 assert!(options.contains(&json!("Enter plan mode")));
117 assert!(options.contains(&json!("Stay in normal mode")));
118 assert_eq!(payload["allow_custom"], false);
119 }
120
121 #[tokio::test]
122 async fn enter_plan_mode_includes_reason() {
123 let tool = EnterPlanModeTool::new();
124 let result = tool
125 .execute(json!({
126 "reason": "This is a complex refactor"
127 }))
128 .await
129 .unwrap();
130
131 assert!(result.success);
132 let payload: serde_json::Value = serde_json::from_str(&result.result).unwrap();
133 let question = payload["question"].as_str().unwrap();
134 assert!(question.contains("This is a complex refactor"));
135 }
136
137 #[tokio::test]
138 async fn enter_plan_mode_accepts_empty_args() {
139 let tool = EnterPlanModeTool::new();
140 let result = tool.execute(json!({})).await.unwrap();
141 assert!(result.success);
142 }
143}