Files
zesdex/apps/infrastructure/src/tools/plan.rs
T

119 lines
3.9 KiB
Rust
Raw Normal View History

//! Plan management tools — enter and mark ready.
//!
//! These tools implement a two-phase planning workflow: `PlanEnter` presents a
//! structured plan to the user for approval, and `PlanReady` signals that the
//! plan is finalised and execution may begin.
use crate::tools::{Tool, ToolCtx};
use anyhow::Result;
use serde_json::{json, Value};
use tracing::{info, instrument, warn};
/// Enter a planning phase — persist a structured plan and notify the user.
///
/// Flow: extract plan text → write to `{session_dir}/PLAN.md` → push a
/// `PlanUpdate` turn event → return plan length summary.
pub struct PlanEnter;
impl Tool for PlanEnter {
fn name(&self) -> &'static str {
"plan_enter"
}
fn description(&self) -> &'static str {
"Enter a planning phase — present a structured plan for approval"
}
fn parameters(&self) -> Value {
json!({
"type": "object",
"properties": {
"plan": {
"type": "string",
"description": "The structured plan text"
}
},
"required": ["plan"]
})
}
#[instrument(skip(self, ctx, args))]
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let plan_text = crate::tools::arg_str(args, "plan")?;
info!(plan_len = plan_text.len(), "plan_enter called");
let plan_path = ctx.session_dir.join("PLAN.md");
let _ = std::fs::write(&plan_path, &plan_text);
if let Some(events) = &ctx.turn_events {
if let Ok(mut q) = events.lock() {
q.push_back(crate::TurnEvent::PlanUpdate(plan_text.clone()));
}
}
Ok(format!(
"Plan entered (length: {} chars). Waiting for approval...",
plan_text.len()
))
}
}
/// Signal that the plan is ready and execution can begin.
///
/// Flow: extract plan content → persist to a timestamped file in
/// `{session_dir}/plans/` → overwrite `PLAN.md` → push `PlanUpdate` event.
pub struct PlanReady;
impl Tool for PlanReady {
fn name(&self) -> &'static str {
"plan_ready"
}
fn description(&self) -> &'static str {
"Signal that the plan is ready and execution can begin"
}
fn parameters(&self) -> Value {
json!({
"type": "object",
"properties": {
"plan": {
"type": "string",
"description": "The final plan content"
}
},
"required": ["plan"]
})
}
#[instrument(skip(self, ctx, args))]
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
let plan_content = crate::tools::arg_str(args, "plan")?;
info!(plan_len = plan_content.len(), "plan_ready called");
// Persist the plan to session directory for reference
let plan_dir = ctx.session_dir.join("plans");
if std::fs::create_dir_all(&plan_dir).is_ok() {
let filename = format!("plan-{}.md", chrono::Utc::now().format("%Y%m%d_%H%M%S"));
let path = plan_dir.join(&filename);
std::fs::write(&path, &plan_content)
.map_err(|e| anyhow::anyhow!("failed to save plan: {e}"))?;
info!(filename = %filename, "plan persisted to disk");
// Also save the latest plan
let plan_path = ctx.session_dir.join("PLAN.md");
let _ = std::fs::write(&plan_path, &plan_content);
if let Some(events) = &ctx.turn_events {
if let Ok(mut q) = events.lock() {
q.push_back(crate::TurnEvent::PlanUpdate(plan_content.clone()));
}
}
Ok(format!("Plan saved to {filename}. Starting execution."))
} else {
warn!("failed to create plans directory");
Ok("Plan is ready. Starting execution.".to_string())
}
}
}