Skip to main content

telos_agent/tools/builtin/
exit_plan_mode.rs

1//! `ExitPlanMode` tool — submit an implementation plan for approval.
2//!
3//! Reads the plan from the file written by the model while in plan mode.
4//! Returns the plan content for user/leader approval. When approved, the
5//! agent exits plan mode and can resume normal write-capable operations.
6
7use async_trait::async_trait;
8use serde_json::{Value, json};
9
10use crate::error::AgentError;
11use crate::tools::api::{Tool, ToolContext, ToolDefinition, ToolOutput};
12
13use super::enter_plan_mode::SharedPlanState;
14
15/// `ExitPlanMode` — read the plan file and submit it.
16pub struct ExitPlanModeTool {
17    plan_state: SharedPlanState,
18}
19
20impl ExitPlanModeTool {
21    pub fn new(plan_state: SharedPlanState) -> Self {
22        Self { plan_state }
23    }
24}
25
26#[async_trait]
27impl Tool for ExitPlanModeTool {
28    fn definition(&self) -> ToolDefinition {
29        ToolDefinition {
30            name: "ExitPlanMode".into(),
31            description: "Submit the implementation plan for approval. Reads from plan file or inline `plan` argument. \
32Use only after writing a complete plan."
33                .into(),
34            input_schema: json!({
35                "type": "object",
36                "properties": {
37                    "plan": {
38                        "type": "string",
39                        "description": "The full plan text (as an alternative to writing to a file)."
40                    }
41                },
42                "additionalProperties": true
43            }),
44        }
45    }
46
47    fn prompt_text(&self) -> Option<&'static str> {
48        Some(EXIT_PLAN_MODE_PROMPT)
49    }
50
51    async fn invoke(
52        &self,
53        arguments: Value,
54        _context: ToolContext,
55    ) -> Result<ToolOutput, AgentError> {
56        // Check if plan mode is actually active
57        let plan_file_path = {
58            let state = self.plan_state.lock().unwrap();
59            if !state.active {
60                return Err(AgentError::Validation(
61                    "ExitPlanMode called outside plan mode. Use EnterPlanMode first.".into(),
62                ));
63            }
64            state.plan_file_path.clone()
65        };
66
67        // Try reading from the inline `plan` argument first
68        let plan_text = if let Some(inline_plan) = arguments.get("plan").and_then(|v| v.as_str()) {
69            inline_plan.to_string()
70        } else if let Some(ref plan_path) = plan_file_path {
71            // Read plan from disk
72            match std::fs::read_to_string(plan_path) {
73                Ok(content) if content.trim().is_empty() => {
74                    return Err(AgentError::Validation(format!(
75                        "Plan file at {} is empty. Write a plan before calling ExitPlanMode.",
76                        plan_path.display()
77                    )));
78                }
79                Ok(content) => content,
80                Err(e) => {
81                    return Err(AgentError::Validation(format!(
82                        "Could not read plan file at {}: {}. Write the plan to this file (using FileWrite) before calling ExitPlanMode, or pass the plan text inline.",
83                        plan_path.display(),
84                        e
85                    )));
86                }
87            }
88        } else {
89            return Err(AgentError::Validation(
90                "No plan file path configured and no inline `plan` argument provided.".into(),
91            ));
92        };
93
94        // Deactivate plan mode
95        {
96            let mut state = self.plan_state.lock().unwrap();
97            state.active = false;
98        }
99
100        Ok(ToolOutput::json(json!({
101            "plan": plan_text,
102            "message": "Plan submitted and approved. Exited plan mode. Resume normal operations — you may now write files, run commands, and implement the plan.",
103            "exited_plan_mode": true
104        })))
105    }
106
107    fn is_concurrency_safe(&self, _: &Value) -> bool {
108        true
109    }
110}
111
112const EXIT_PLAN_MODE_PROMPT: &str = r#"ExitPlanMode submits your plan for approval.
113
114Call after writing a complete plan to the plan file (via Write). Include: problem, context, approach, steps, affected files, risks. Pass the plan inline via `plan` argument or let it read from disk. After approval, begin implementing — don't re-explain the plan unless asked."#;