bamboo_tools/tools/
enter_plan_mode.rs1use async_trait::async_trait;
2use bamboo_agent_core::{Tool, ToolCtx, ToolError, ToolOutcome, 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 invoke(
50 &self,
51 args: serde_json::Value,
52 _ctx: ToolCtx,
53 ) -> Result<ToolOutcome, ToolError> {
54 let parsed: EnterPlanModeArgs = serde_json::from_value(args).map_err(|e| {
55 ToolError::InvalidArguments(format!("Invalid EnterPlanMode args: {}", e))
56 })?;
57
58 let question = if let Some(ref reason) = parsed.reason {
59 format!(
60 "Enter plan mode? The assistant wants to switch to read-only exploration to design an approach before any changes. Reason: {}",
61 reason
62 )
63 } else {
64 "Enter plan mode? The assistant will switch to read-only exploration to design an approach before any changes.".to_string()
65 };
66
67 let payload = json!({
68 "status": "awaiting_user_input",
69 "question": question,
70 "options": ["Enter plan mode", "Stay in normal mode"],
71 "allow_custom": false,
72 });
73
74 Ok(ToolOutcome::Completed(ToolResult {
75 success: true,
76 result: payload.to_string(),
77 display_preference: Some("conclusion_with_options".to_string()),
78 images: Vec::new(),
79 }))
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86 use serde_json::json;
87
88 #[test]
89 fn enter_plan_mode_has_correct_name() {
90 let tool = EnterPlanModeTool::new();
91 assert_eq!(tool.name(), "EnterPlanMode");
92 }
93
94 #[test]
95 fn enter_plan_mode_has_description() {
96 let tool = EnterPlanModeTool::new();
97 assert!(!tool.description().is_empty());
98 assert!(tool.description().contains("plan"));
99 }
100
101 #[tokio::test]
102 async fn enter_plan_mode_returns_conclusion_with_options() {
103 let tool = EnterPlanModeTool::new();
104 let out = tool.invoke(json!({}), ToolCtx::none("t")).await.unwrap();
105 let ToolOutcome::Completed(result) = out else {
106 panic!("expected Completed")
107 };
108
109 assert!(result.success);
110 assert_eq!(
111 result.display_preference,
112 Some("conclusion_with_options".to_string())
113 );
114
115 let payload: serde_json::Value = serde_json::from_str(&result.result).unwrap();
116 assert_eq!(payload["status"], "awaiting_user_input");
117 assert!(payload["question"]
118 .as_str()
119 .unwrap()
120 .contains("Enter plan mode"));
121 let options = payload["options"].as_array().unwrap();
122 assert_eq!(options.len(), 2);
123 assert!(options.contains(&json!("Enter plan mode")));
124 assert!(options.contains(&json!("Stay in normal mode")));
125 assert_eq!(payload["allow_custom"], false);
126 }
127
128 #[tokio::test]
129 async fn enter_plan_mode_includes_reason() {
130 let tool = EnterPlanModeTool::new();
131 let out = tool
132 .invoke(
133 json!({
134 "reason": "This is a complex refactor"
135 }),
136 ToolCtx::none("t"),
137 )
138 .await
139 .unwrap();
140 let ToolOutcome::Completed(result) = out else {
141 panic!("expected Completed")
142 };
143
144 assert!(result.success);
145 let payload: serde_json::Value = serde_json::from_str(&result.result).unwrap();
146 let question = payload["question"].as_str().unwrap();
147 assert!(question.contains("This is a complex refactor"));
148 }
149
150 #[tokio::test]
151 async fn enter_plan_mode_accepts_empty_args() {
152 let tool = EnterPlanModeTool::new();
153 let out = tool.invoke(json!({}), ToolCtx::none("t")).await.unwrap();
154 let ToolOutcome::Completed(result) = out else {
155 panic!("expected Completed")
156 };
157 assert!(result.success);
158 }
159}