From 6ce0a5875bbde55a00df054e7f0bceba681cf44d Mon Sep 17 00:00:00 2001 From: easong-openai Date: Thu, 31 Jul 2025 13:45:52 -0700 Subject: [PATCH] Initial planning tool (#1753) We need to optimize the prompt, but this causes the model to use the new planning_tool. image --- codex-rs/core/prompt.md | 9 +++ codex-rs/tui/src/chatwidget.rs | 4 ++ codex-rs/tui/src/history_cell.rs | 96 ++++++++++++++++++++++++++++++++ codex-rs/tui/src/lib.rs | 2 +- 4 files changed, 110 insertions(+), 1 deletion(-) diff --git a/codex-rs/core/prompt.md b/codex-rs/core/prompt.md index 66cd55b6..0a457827 100644 --- a/codex-rs/core/prompt.md +++ b/codex-rs/core/prompt.md @@ -96,3 +96,12 @@ You can invoke apply_patch like: ``` shell {"command":["apply_patch","*** Begin Patch\n*** Add File: hello.txt\n+Hello, world!\n*** End Patch\n"]} ``` + +Plan updates + +A tool named `update_plan` is available. Use it to keep an up‑to‑date, step‑by‑step plan for the task so you can follow your progress. When making your plans, keep in mind that you are a deployed coding agent - `update_plan` calls should not involve doing anything that you aren't capable of doing. For example, `update_plan` calls should NEVER contain tasks to merge your own pull requests. Only stop to ask the user if you genuinely need their feedback on a change. + +- At the start of the task, call `update_plan` with an initial plan: a short list of 1‑sentence steps with a `status` for each step (`pending`, `in_progress`, or `completed`). There should always be exactly one `in_progress` step until everything is done. +- Whenever you finish a step, call `update_plan` again, marking the finished step as `completed` and the next step as `in_progress`. +- If your plan needs to change, call `update_plan` with the revised steps and include an `explanation` describing the change. +- When all steps are complete, make a final `update_plan` call with all steps marked `completed`. diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 3ee724e6..50488f8b 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -293,6 +293,10 @@ impl ChatWidget<'_> { self.add_to_history(HistoryCell::new_error_event(message.clone())); self.bottom_pane.set_task_running(false); } + EventMsg::PlanUpdate(update) => { + self.add_to_history(HistoryCell::new_plan_update(update)); + self.request_redraw(); + } EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent { call_id: _, command, diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 956a0cc7..0e9f2533 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -9,6 +9,9 @@ use codex_common::summarize_sandbox_policy; use codex_core::WireApi; use codex_core::config::Config; use codex_core::model_supports_reasoning_summaries; +use codex_core::plan_tool::PlanItemArg; +use codex_core::plan_tool::StepStatus; +use codex_core::plan_tool::UpdatePlanArgs; use codex_core::protocol::FileChange; use codex_core::protocol::McpInvocation; use codex_core::protocol::SessionConfiguredEvent; @@ -109,6 +112,10 @@ pub(crate) enum HistoryCell { /// behaviour of `ActiveExecCommand` so the user sees *what* patch the /// model wants to apply before being prompted to approve or deny it. PendingPatch { view: TextBlock }, + + /// A human‑friendly rendering of the model's current plan and step + /// statuses provided via the `update_plan` tool. + PlanUpdate { view: TextBlock }, } const TOOL_CALL_MAX_LINES: usize = 5; @@ -130,6 +137,7 @@ impl HistoryCell { | HistoryCell::CompletedExecCommand { view } | HistoryCell::CompletedMcpToolCall { view } | HistoryCell::PendingPatch { view } + | HistoryCell::PlanUpdate { view } | HistoryCell::ActiveExecCommand { view, .. } | HistoryCell::ActiveMcpToolCall { view, .. } => { view.lines.iter().map(line_to_static).collect() @@ -477,6 +485,94 @@ impl HistoryCell { } } + /// Render a user‑friendly plan update with colourful status icons and a + /// simple progress indicator so users can follow along. + pub(crate) fn new_plan_update(update: UpdatePlanArgs) -> Self { + let UpdatePlanArgs { explanation, plan } = update; + + let mut lines: Vec> = Vec::new(); + + // Title + lines.push(Line::from("plan".magenta().bold())); + + if !plan.is_empty() { + // Progress bar – show completed/total with a visual bar + let total = plan.len(); + let completed = plan + .iter() + .filter(|p| matches!(p.status, StepStatus::Completed)) + .count(); + let width: usize = 20; + let filled = (completed * width + total / 2) / total; + let empty = width.saturating_sub(filled); + let mut bar_spans: Vec = Vec::new(); + if filled > 0 { + bar_spans.push(Span::styled( + "█".repeat(filled), + Style::default().fg(Color::Green), + )); + } + if empty > 0 { + bar_spans.push(Span::styled( + "░".repeat(empty), + Style::default().fg(Color::Gray), + )); + } + let progress_prefix = Span::raw("progress ["); + let progress_suffix = Span::raw("] "); + let fraction = Span::raw(format!("{completed}/{total}")); + let mut progress_line_spans = vec![progress_prefix]; + progress_line_spans.extend(bar_spans); + progress_line_spans.push(progress_suffix); + progress_line_spans.push(fraction); + lines.push(Line::from(progress_line_spans)); + } + + // Optional explanation/note from the model + if let Some(expl) = explanation.and_then(|s| { + let t = s.trim().to_string(); + if t.is_empty() { None } else { Some(t) } + }) { + lines.push(Line::from("note".gray().italic())); + for l in expl.lines() { + lines.push(Line::from(l.to_string()).gray()); + } + } + + // Steps (1‑based numbering) with fun, readable status icons + if plan.is_empty() { + lines.push(Line::from("(no steps provided)".gray().italic())); + } else { + for (idx, PlanItemArg { step, status }) in plan.into_iter().enumerate() { + let num = idx + 1; + let (icon, style): (&str, Style) = match status { + StepStatus::Completed => ("✓", Style::default().fg(Color::Green)), + StepStatus::InProgress => ( + "▶", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), + StepStatus::Pending => ("○", Style::default().fg(Color::Gray)), + }; + let prefix = vec![ + Span::raw(format!("{num:>2}. [")), + Span::styled(icon.to_string(), style), + Span::raw("] "), + ]; + let mut spans = prefix; + spans.push(Span::raw(step)); + lines.push(Line::from(spans)); + } + } + + lines.push(Line::from("")); + + HistoryCell::PlanUpdate { + view: TextBlock::new(lines), + } + } + /// Create a new `PendingPatch` cell that lists the file‑level summary of /// a proposed patch. The summary lines should already be formatted (e.g. /// "A path/to/file.rs"). diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 7e987f6f..f0a0e9d8 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -75,7 +75,7 @@ pub async fn run_main( config_profile: cli.config_profile.clone(), codex_linux_sandbox_exe, base_instructions: None, - include_plan_tool: None, + include_plan_tool: Some(true), }; // Parse `-c` overrides from the CLI. let cli_kv_overrides = match cli.config_overrides.parse_overrides() {