telos_agent/tools/builtin/
exit_plan_mode.rs1use 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
15pub 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 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 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 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 {
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."#;