Skip to main content

bamboo_tools/tools/
enter_plan_mode.rs

1use 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}