Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ pub struct Agent<P: ModelProvider> {
pub plan_tool_enforcement: PlanToolEnforcementMode,
pub mcp_pin_enforcement: McpPinEnforcementMode,
pub plan_step_constraints: Vec<PlanStepConstraint>,
pub current_plan: Vec<crate::tools::PlanItem>,
pub tool_call_budget: ToolCallBudget,
pub mcp_runtime_trace: Vec<McpRuntimeTraceEntry>,
pub operator_queue: PendingMessageQueue,
Expand Down
55 changes: 55 additions & 0 deletions src/agent/tool_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,58 @@ impl<P: ModelProvider> Agent<P> {
})
}

pub(super) fn apply_update_plan_tool_success(
&mut self,
run_id: &str,
step: u32,
tc: &ToolCall,
) {
let Ok(update) = crate::tools::parse_update_plan_args(&tc.arguments) else {
return;
};
self.current_plan = update.items.clone();
let in_progress = self
.current_plan
.iter()
.find(|item| matches!(item.status, crate::tools::PlanStatus::InProgress))
.map(|item| item.step.clone());
let pending = self
.current_plan
.iter()
.filter(|item| matches!(item.status, crate::tools::PlanStatus::Pending))
.count();
let completed = self
.current_plan
.iter()
.filter(|item| matches!(item.status, crate::tools::PlanStatus::Completed))
.count();
let items = self
.current_plan
.iter()
.map(|item| {
serde_json::json!({
"step": item.step,
"status": item.status.as_str()
})
})
.collect::<Vec<_>>();
self.emit_event(
run_id,
step,
EventKind::PlanUpdated,
serde_json::json!({
"tool_call_id": tc.id,
"name": tc.name,
"explanation": update.explanation,
"items": items,
"item_count": self.current_plan.len(),
"pending": pending,
"completed": completed,
"in_progress": in_progress
}),
);
}

#[allow(clippy::too_many_arguments)]
pub(super) fn handle_schema_repair_attempt(
&mut self,
Expand Down Expand Up @@ -1593,6 +1645,9 @@ impl<P: ModelProvider> Agent<P> {
} else {
None
};
if final_ok && tc.name == "update_plan" {
self.apply_update_plan_tool_success(&run_id, step, tc);
}
self.update_taint_for_tool_result(&run_id, step, tc, &content, messages.len(), taint_state);
self.record_allowed_tool_result(
&run_id,
Expand Down
1 change: 1 addition & 0 deletions src/agent_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ pub(crate) async fn run_agent_with_ui<P: ModelProvider>(
plan_tool_enforcement: effective_plan_tool_enforcement,
mcp_pin_enforcement: args.mcp_pin_enforcement,
plan_step_constraints,
current_plan: Vec::new(),
tool_call_budget: ToolCallBudget {
max_wall_time_ms: if args.no_limits {
0
Expand Down
49 changes: 49 additions & 0 deletions src/agent_tests.rs

Large diffs are not rendered by default.

44 changes: 42 additions & 2 deletions src/chat_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ mod overlay;
use overlay::{draw_learn_overlay, render_with_caret};
pub(crate) use overlay::{LearnOverlayRenderModel, LearnOverlaySummaryChoice, LearnOverlayTab};

use crate::tui::state::{ToolRow, UiState};
use crate::tui::state::{PlanRow, ToolRow, UiState};

#[allow(clippy::too_many_arguments)]
pub(crate) fn draw_chat_frame(
Expand Down Expand Up @@ -133,7 +133,8 @@ pub(crate) fn draw_chat_frame(
outer[1],
);

let has_side = show_tools || show_approvals || show_thinking_panel;
let has_plan = !ui_state.plan_items.is_empty();
let has_side = has_plan || show_tools || show_approvals || show_thinking_panel;
let (chat_area, separator_area, side_area) = if has_side {
let cols = Layout::default()
.direction(Direction::Horizontal)
Expand Down Expand Up @@ -214,11 +215,15 @@ pub(crate) fn draw_chat_frame(
if let Some(side) = side_area {
#[derive(Clone, Copy)]
enum SidePane {
Plan,
Tools,
Approvals,
Reasoning,
}
let mut panes = Vec::new();
if has_plan {
panes.push(SidePane::Plan);
}
if show_tools {
panes.push(SidePane::Tools);
}
Expand All @@ -242,6 +247,7 @@ pub(crate) fn draw_chat_frame(
for (idx, pane) in panes.iter().enumerate() {
let area = splits[idx];
match pane {
SidePane::Plan => draw_plan_pane(f, area, &ui_state.plan_items),
SidePane::Tools => draw_tools_pane(
f,
area,
Expand Down Expand Up @@ -454,6 +460,40 @@ pub(crate) fn draw_chat_frame(
}
}

fn draw_plan_pane(f: &mut ratatui::Frame<'_>, area: ratatui::layout::Rect, items: &[PlanRow]) {
let completed = items
.iter()
.filter(|item| item.status == "completed")
.count();
let title = format!("Plan {completed}/{}", items.len());
let body = items
.iter()
.map(|item| {
let mark = match item.status.as_str() {
"completed" => "x",
"in_progress" => ">",
_ => " ",
};
format!("[{mark}] {}", item.step)
})
.collect::<Vec<_>>()
.join("\n");
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(1)])
.split(area);
f.render_widget(
Paragraph::new(title).style(Style::default().fg(Color::DarkGray)),
layout[0],
);
f.render_widget(
Paragraph::new(body)
.style(Style::default().fg(Color::DarkGray))
.wrap(Wrap { trim: false }),
layout[1],
);
}

fn draw_tools_pane(
f: &mut ratatui::Frame<'_>,
area: ratatui::layout::Rect,
Expand Down
1 change: 1 addition & 0 deletions src/eval/runner_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,7 @@ pub(crate) async fn run_single(
plan_tool_enforcement: crate::agent::PlanToolEnforcementMode::Off,
mcp_pin_enforcement: crate::agent::McpPinEnforcementMode::Hard,
plan_step_constraints: Vec::new(),
current_plan: Vec::new(),
tool_call_budget: ToolCallBudget {
max_wall_time_ms: task_max_wall_time_ms,
max_total_tool_calls: 0,
Expand Down
1 change: 1 addition & 0 deletions src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub enum EventKind {
ToolExecStart,
ToolExecEnd,
ShellOutputChunk,
PlanUpdated,
PostWriteVerifyStart,
PostWriteVerifyEnd,
ToolRetry,
Expand Down
4 changes: 4 additions & 0 deletions src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::types::{Message, SideEffects, ToolCall};
mod catalog;
mod envelope;
mod exec_fs;
mod exec_plan;
mod exec_shell;
mod exec_support;
mod exec_write;
Expand All @@ -22,6 +23,8 @@ pub use envelope::{
envelope_to_message, invalid_args_tool_message, to_tool_result_envelope,
to_tool_result_envelope_with_error,
};
pub(crate) use exec_plan::parse_update_plan_args;
pub use exec_plan::{PlanItem, PlanStatus};
use exec_support::ToolExecution;
pub use schema::{
compact_builtin_schema, invalid_args_detail, minimal_builtin_example,
Expand Down Expand Up @@ -198,6 +201,7 @@ pub async fn execute_tool_streaming(
"read_file" => exec_fs::run_read_file(rt, &normalized_args).await,
"glob" => exec_fs::run_glob(rt, &normalized_args).await,
"grep" => exec_fs::run_grep(rt, &normalized_args).await,
"update_plan" => exec_plan::run_update_plan(rt, &normalized_args).await,
"shell" => exec_shell::run_shell(rt, &normalized_args, shell_stream).await,
"write_file" => exec_write::run_write_file(rt, &normalized_args).await,
"apply_patch" => exec_write::run_apply_patch(rt, &normalized_args).await,
Expand Down
26 changes: 26 additions & 0 deletions src/tools/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::types::{SideEffects, ToolDef};
pub fn tool_side_effects(tool_name: &str) -> SideEffects {
match tool_name {
"list_dir" | "read_file" | "glob" | "grep" => SideEffects::FilesystemRead,
"update_plan" => SideEffects::None,
"shell" => SideEffects::ShellExec,
"write_file" | "apply_patch" | "edit" | "str_replace" => SideEffects::FilesystemWrite,
_ if tool_name.starts_with("mcp.playwright.") => SideEffects::Browser,
Expand Down Expand Up @@ -64,6 +65,31 @@ pub fn builtin_tools_enabled(enable_write_tools: bool, enable_shell_tool: bool)
}),
side_effects: SideEffects::FilesystemRead,
},
ToolDef {
name: "update_plan".to_string(),
description: "Update the current in-run plan. Provide the full current list of steps with status pending, in_progress, or completed; at most one item may be in_progress.".to_string(),
parameters: json!({
"type":"object",
"properties":{
"explanation":{"type":"string"},
"items":{
"type":"array",
"items":{
"type":"object",
"properties":{
"step":{"type":"string"},
"status":{"type":"string","enum":["pending","in_progress","completed"]}
},
"required":["step","status"]
},
"minItems":1,
"maxItems":20
}
},
"required":["items"]
}),
side_effects: SideEffects::None,
},
];
if enable_shell_tool {
tools.push(ToolDef {
Expand Down
Loading
Loading