From b99200fbaf4ed416ae78cc2774c0006c0e51a405 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Sat, 21 Mar 2026 00:03:32 +0100 Subject: [PATCH] agent: Add update_plan tool (#52048) ## Context Adds a tool to utilize the UI we already expose to ACP agents. Behind a feature flag for now. ## How to Review Mostly a tool to hook up to all of the plan plumbing we already have in acp thread. ## Self-Review Checklist - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - N/A --- assets/settings/default.json | 2 + crates/agent/src/agent.rs | 3 + crates/agent/src/templates.rs | 1 + crates/agent/src/templates/system_prompt.hbs | 28 ++ crates/agent/src/tests/mod.rs | 173 ++++++++--- crates/agent/src/thread.rs | 29 +- crates/agent/src/tools.rs | 3 + crates/agent/src/tools/update_plan_tool.rs | 290 ++++++++++++++++++ crates/eval/src/example.rs | 3 + crates/feature_flags/src/flags.rs | 10 + .../src/pages/tool_permissions_setup.rs | 3 + 11 files changed, 500 insertions(+), 45 deletions(-) create mode 100644 crates/agent/src/tools/update_plan_tool.rs diff --git a/assets/settings/default.json b/assets/settings/default.json index c86f0bff80ecef0f428794f5a07eadf6704b0872..be1244bd14dc98005e5ba6ecaf5392af2fff9b24 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1073,6 +1073,7 @@ "spawn_agent": true, "terminal": true, "thinking": true, + "update_plan": true, "web_search": true, }, }, @@ -1092,6 +1093,7 @@ "grep": true, "spawn_agent": true, "thinking": true, + "update_plan": true, "web_search": true, }, }, diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index bf756d433f0ca3812b5c5419dfef82e4d9760093..62a26f5b10672e3d1367d0fb7b085602a049df47 100644 --- a/crates/agent/src/agent.rs +++ b/crates/agent/src/agent.rs @@ -1219,6 +1219,9 @@ impl NativeAgentConnection { thread.update_tool_call(update, cx) })??; } + ThreadEvent::Plan(plan) => { + acp_thread.update(cx, |thread, cx| thread.update_plan(plan, cx))?; + } ThreadEvent::SubagentSpawned(session_id) => { acp_thread.update(cx, |thread, cx| { thread.subagent_spawned(session_id, cx); diff --git a/crates/agent/src/templates.rs b/crates/agent/src/templates.rs index db787d834e63746fdbea9e837f4fd0615f85c984..103fde17fd4d865b346a428e1f23e335005afe88 100644 --- a/crates/agent/src/templates.rs +++ b/crates/agent/src/templates.rs @@ -85,6 +85,7 @@ mod tests { let templates = Templates::new(); let rendered = template.render(&templates).unwrap(); assert!(rendered.contains("## Fixing Diagnostics")); + assert!(!rendered.contains("## Planning")); assert!(rendered.contains("test-model")); } } diff --git a/crates/agent/src/templates/system_prompt.hbs b/crates/agent/src/templates/system_prompt.hbs index d65df9078642153b7f82d454d5b3caeb8ef0c8ad..67c920707289173ac4c7c1c9d98a8cd64126eb89 100644 --- a/crates/agent/src/templates/system_prompt.hbs +++ b/crates/agent/src/templates/system_prompt.hbs @@ -20,6 +20,34 @@ You are a highly skilled software engineer with extensive knowledge in many prog - When running commands that may run indefinitely or for a long time (such as build scripts, tests, servers, or file watchers), specify `timeout_ms` to bound runtime. If the command times out, the user can always ask you to run it again with a longer timeout or no timeout if they're willing to wait or cancel manually. - Avoid HTML entity escaping - use plain characters instead. +{{#if (contains available_tools 'update_plan') }} +## Planning + +- You have access to an `update_plan` tool which tracks steps and progress and renders them to the user. +- Use it to show that you've understood the task and to make complex, ambiguous, or multi-phase work easier for the user to follow. +- A good plan breaks the work into meaningful, logically ordered steps that are easy to verify as you go. +- When writing a plan, prefer a short list of concise, concrete steps. +- Keep each step focused on a real unit of work and use short 1-sentence descriptions. +- Do not use plans for simple or single-step queries that you can just do or answer immediately. +- Do not use plans to pad your response with filler steps or to state the obvious. +- Do not include steps that you are not actually capable of doing. +- After calling `update_plan`, do not repeat the full plan in your response. The UI already displays it. Instead, briefly summarize what changed and note any important context or next step. +- Before moving on to a new phase of work, mark the previous step as completed when appropriate. +- When work is in progress, prefer having exactly one step marked as `in_progress`. +- You can mark multiple completed steps in a single `update_plan` call. +- If the task changes midway through, update the plan so it reflects the new approach. + +Use a plan when: + +- The task is non-trivial and will require multiple actions over a longer horizon. +- There are logical phases or dependencies where sequencing matters. +- The work has ambiguity that benefits from outlining high-level goals. +- You want intermediate checkpoints for feedback and validation. +- The user asked you to do more than one thing in a single prompt. +- The user asked you to use the plan tool or TODOs. +- You discover additional steps while working and intend to complete them before yielding to the user. + +{{/if}} ## Searching and Reading If you are unsure how to fulfill the user's request, gather more information with tool calls and/or clarifying questions. diff --git a/crates/agent/src/tests/mod.rs b/crates/agent/src/tests/mod.rs index 31563729cfd361991832eeeb3428043c40b81e79..d486f4f667a91fd18e5f5cade1933f2527e8048f 100644 --- a/crates/agent/src/tests/mod.rs +++ b/crates/agent/src/tests/mod.rs @@ -999,6 +999,20 @@ async fn expect_tool_call_update_fields( } } +async fn expect_plan(events: &mut UnboundedReceiver>) -> acp::Plan { + let event = events + .next() + .await + .expect("no plan event received") + .unwrap(); + match event { + ThreadEvent::Plan(plan) => plan, + event => { + panic!("Unexpected event {event:?}"); + } + } +} + async fn next_tool_call_authorization( events: &mut UnboundedReceiver>, ) -> ToolCallAuthorization { @@ -3416,6 +3430,122 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) { ); } +#[gpui::test] +async fn test_update_plan_tool_updates_thread_events(cx: &mut TestAppContext) { + let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; + thread.update(cx, |thread, _cx| thread.add_tool(UpdatePlanTool)); + let fake_model = model.as_fake(); + + let mut events = thread + .update(cx, |thread, cx| { + thread.send(UserMessageId::new(), ["Make a plan"], cx) + }) + .unwrap(); + cx.run_until_parked(); + + let input = json!({ + "plan": [ + { + "step": "Inspect the code", + "status": "completed", + "priority": "high" + }, + { + "step": "Implement the tool", + "status": "in_progress" + }, + { + "step": "Run tests", + "status": "pending", + "priority": "low" + } + ] + }); + fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( + LanguageModelToolUse { + id: "plan_1".into(), + name: UpdatePlanTool::NAME.into(), + raw_input: input.to_string(), + input, + is_input_complete: true, + thought_signature: None, + }, + )); + fake_model.end_last_completion_stream(); + cx.run_until_parked(); + + let tool_call = expect_tool_call(&mut events).await; + assert_eq!( + tool_call, + acp::ToolCall::new("plan_1", "Update plan") + .kind(acp::ToolKind::Think) + .raw_input(json!({ + "plan": [ + { + "step": "Inspect the code", + "status": "completed", + "priority": "high" + }, + { + "step": "Implement the tool", + "status": "in_progress" + }, + { + "step": "Run tests", + "status": "pending", + "priority": "low" + } + ] + })) + .meta(acp::Meta::from_iter([( + "tool_name".into(), + "update_plan".into() + )])) + ); + + let update = expect_tool_call_update_fields(&mut events).await; + assert_eq!( + update, + acp::ToolCallUpdate::new( + "plan_1", + acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::InProgress) + ) + ); + + let plan = expect_plan(&mut events).await; + assert_eq!( + plan, + acp::Plan::new(vec![ + acp::PlanEntry::new( + "Inspect the code", + acp::PlanEntryPriority::High, + acp::PlanEntryStatus::Completed, + ), + acp::PlanEntry::new( + "Implement the tool", + acp::PlanEntryPriority::Medium, + acp::PlanEntryStatus::InProgress, + ), + acp::PlanEntry::new( + "Run tests", + acp::PlanEntryPriority::Low, + acp::PlanEntryStatus::Pending, + ), + ]) + ); + + let update = expect_tool_call_update_fields(&mut events).await; + assert_eq!( + update, + acp::ToolCallUpdate::new( + "plan_1", + acp::ToolCallUpdateFields::new() + .status(acp::ToolCallStatus::Completed) + .raw_output("Plan updated") + ) + ); +} + #[gpui::test] async fn test_send_no_retry_on_success(cx: &mut TestAppContext) { let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; @@ -3822,6 +3952,7 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { StreamingEchoTool::NAME: true, StreamingFailingEchoTool::NAME: true, TerminalTool::NAME: true, + UpdatePlanTool::NAME: true, } } } @@ -5011,48 +5142,6 @@ async fn test_subagent_tool_resume_session(cx: &mut TestAppContext) { ); } -#[gpui::test] -async fn test_subagent_tool_is_present_when_feature_flag_enabled(cx: &mut TestAppContext) { - init_test(cx); - - cx.update(|cx| { - cx.update_flags(true, vec!["subagents".to_string()]); - }); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/test"), json!({})).await; - let project = Project::test(fs, [path!("/test").as_ref()], cx).await; - let project_context = cx.new(|_cx| ProjectContext::default()); - let context_server_store = project.read_with(cx, |project, _| project.context_server_store()); - let context_server_registry = - cx.new(|cx| ContextServerRegistry::new(context_server_store.clone(), cx)); - let model = Arc::new(FakeLanguageModel::default()); - - let environment = Rc::new(cx.update(|cx| { - FakeThreadEnvironment::default().with_terminal(FakeTerminalHandle::new_never_exits(cx)) - })); - - let thread = cx.new(|cx| { - let mut thread = Thread::new( - project.clone(), - project_context, - context_server_registry, - Templates::new(), - Some(model), - cx, - ); - thread.add_default_tools(environment, cx); - thread - }); - - thread.read_with(cx, |thread, _| { - assert!( - thread.has_registered_tool(SpawnAgentTool::NAME), - "subagent tool should be present when feature flag is enabled" - ); - }); -} - #[gpui::test] async fn test_subagent_thread_inherits_parent_thread_properties(cx: &mut TestAppContext) { init_test(cx); diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index bf4f923b343bf45c9f8a8bf45c1788f09e18c7f3..5e1de6783953a53a92196823e79b168ee9f08319 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -3,12 +3,14 @@ use crate::{ DeletePathTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MovePathTool, NowTool, OpenTool, ProjectSnapshot, ReadFileTool, RestoreFileFromDiskTool, SaveFileTool, SpawnAgentTool, StreamingEditFileTool, - SystemPromptTemplate, Template, Templates, TerminalTool, ToolPermissionDecision, WebSearchTool, - decide_permission_from_settings, + SystemPromptTemplate, Template, Templates, TerminalTool, ToolPermissionDecision, + UpdatePlanTool, WebSearchTool, decide_permission_from_settings, }; use acp_thread::{MentionUri, UserMessageId}; use action_log::ActionLog; -use feature_flags::{FeatureFlagAppExt as _, StreamingEditFileToolFeatureFlag}; +use feature_flags::{ + FeatureFlagAppExt as _, StreamingEditFileToolFeatureFlag, UpdatePlanToolFeatureFlag, +}; use agent_client_protocol as acp; use agent_settings::{ @@ -661,6 +663,7 @@ pub enum ThreadEvent { AgentThinking(String), ToolCall(acp::ToolCall), ToolCallUpdate(acp_thread::ToolCallUpdate), + Plan(acp::Plan), ToolCallAuthorization(ToolCallAuthorization), SubagentSpawned(acp::SessionId), Retry(acp_thread::RetryStatus), @@ -1529,6 +1532,9 @@ impl Thread { self.add_tool(MovePathTool::new(self.project.clone())); self.add_tool(NowTool); self.add_tool(OpenTool::new(self.project.clone())); + if cx.has_flag::() { + self.add_tool(UpdatePlanTool); + } self.add_tool(ReadFileTool::new( self.project.clone(), self.action_log.clone(), @@ -3476,6 +3482,10 @@ impl ThreadEventStream { .ok(); } + fn send_plan(&self, plan: acp::Plan) { + self.0.unbounded_send(Ok(ThreadEvent::Plan(plan))).ok(); + } + fn send_retry(&self, status: acp_thread::RetryStatus) { self.0.unbounded_send(Ok(ThreadEvent::Retry(status))).ok(); } @@ -3611,6 +3621,10 @@ impl ToolCallEventStream { .ok(); } + pub fn update_plan(&self, plan: acp::Plan) { + self.stream.send_plan(plan); + } + /// Authorize a third-party tool (e.g., MCP tool from a context server). /// /// Unlike built-in tools, third-party tools don't support pattern-based permissions. @@ -3899,6 +3913,15 @@ impl ToolCallEventStreamReceiver { panic!("Expected terminal but got: {:?}", event); } } + + pub async fn expect_plan(&mut self) -> acp::Plan { + let event = self.0.next().await; + if let Some(Ok(ThreadEvent::Plan(plan))) = event { + plan + } else { + panic!("Expected plan but got: {:?}", event); + } + } } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/agent/src/tools.rs b/crates/agent/src/tools.rs index 446472e0c459aa15fa57bb8b49178b08e6781d11..f172fd3fdbe14babb77e53b63dd79aebf50d2603 100644 --- a/crates/agent/src/tools.rs +++ b/crates/agent/src/tools.rs @@ -19,6 +19,7 @@ mod streaming_edit_file_tool; mod terminal_tool; mod tool_edit_parser; mod tool_permissions; +mod update_plan_tool; mod web_search_tool; use crate::AgentTool; @@ -44,6 +45,7 @@ pub use spawn_agent_tool::*; pub use streaming_edit_file_tool::*; pub use terminal_tool::*; pub use tool_permissions::*; +pub use update_plan_tool::*; pub use web_search_tool::*; macro_rules! tools { @@ -132,5 +134,6 @@ tools! { SaveFileTool, SpawnAgentTool, TerminalTool, + UpdatePlanTool, WebSearchTool, } diff --git a/crates/agent/src/tools/update_plan_tool.rs b/crates/agent/src/tools/update_plan_tool.rs new file mode 100644 index 0000000000000000000000000000000000000000..9fdc5a865dfb5cd2a18e3f24b3f7544b397588d3 --- /dev/null +++ b/crates/agent/src/tools/update_plan_tool.rs @@ -0,0 +1,290 @@ +use crate::{AgentTool, ToolCallEventStream, ToolInput}; +use agent_client_protocol as acp; +use gpui::{App, SharedString, Task}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +#[schemars(inline)] +pub enum PlanEntryStatus { + /// The task has not started yet. + Pending, + /// The task is currently being worked on. + InProgress, + /// The task has been successfully completed. + Completed, +} + +impl From for acp::PlanEntryStatus { + fn from(value: PlanEntryStatus) -> Self { + match value { + PlanEntryStatus::Pending => acp::PlanEntryStatus::Pending, + PlanEntryStatus::InProgress => acp::PlanEntryStatus::InProgress, + PlanEntryStatus::Completed => acp::PlanEntryStatus::Completed, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] +#[schemars(inline)] +pub enum PlanEntryPriority { + High, + #[default] + Medium, + Low, +} + +impl From for acp::PlanEntryPriority { + fn from(value: PlanEntryPriority) -> Self { + match value { + PlanEntryPriority::High => acp::PlanEntryPriority::High, + PlanEntryPriority::Medium => acp::PlanEntryPriority::Medium, + PlanEntryPriority::Low => acp::PlanEntryPriority::Low, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct PlanItem { + /// Human-readable description of what this task aims to accomplish. + pub step: String, + /// The current status of this task. + pub status: PlanEntryStatus, + /// The relative importance of this task. Defaults to medium when omitted. + #[serde(default)] + pub priority: PlanEntryPriority, +} + +impl From for acp::PlanEntry { + fn from(value: PlanItem) -> Self { + acp::PlanEntry::new(value.step, value.priority.into(), value.status.into()) + } +} + +/// Updates the task plan. +/// Provide a list of plan entries, each with step, status, and optional priority. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct UpdatePlanToolInput { + /// The list of plan entries and their current statuses. + pub plan: Vec, +} + +pub struct UpdatePlanTool; + +impl UpdatePlanTool { + fn to_plan(input: UpdatePlanToolInput) -> acp::Plan { + acp::Plan::new(input.plan.into_iter().map(Into::into).collect()) + } +} + +impl AgentTool for UpdatePlanTool { + type Input = UpdatePlanToolInput; + type Output = String; + + const NAME: &'static str = "update_plan"; + + fn kind() -> acp::ToolKind { + acp::ToolKind::Think + } + + fn initial_title( + &self, + input: Result, + _cx: &mut App, + ) -> SharedString { + match input { + Ok(input) if input.plan.is_empty() => "Clear plan".into(), + Ok(_) | Err(_) => "Update plan".into(), + } + } + + fn run( + self: Arc, + input: ToolInput, + event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Task> { + cx.spawn(async move |_cx| { + let input = input + .recv() + .await + .map_err(|e| format!("Failed to receive tool input: {e}"))?; + + event_stream.update_plan(Self::to_plan(input)); + + Ok("Plan updated".to_string()) + }) + } + + fn replay( + &self, + input: Self::Input, + _output: Self::Output, + event_stream: ToolCallEventStream, + _cx: &mut App, + ) -> anyhow::Result<()> { + event_stream.update_plan(Self::to_plan(input)); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ToolCallEventStream; + use gpui::TestAppContext; + use pretty_assertions::assert_eq; + + fn sample_input() -> UpdatePlanToolInput { + UpdatePlanToolInput { + plan: vec![ + PlanItem { + step: "Inspect the existing tool wiring".to_string(), + status: PlanEntryStatus::Completed, + priority: PlanEntryPriority::High, + }, + PlanItem { + step: "Implement the update_plan tool".to_string(), + status: PlanEntryStatus::InProgress, + priority: PlanEntryPriority::Medium, + }, + PlanItem { + step: "Add tests".to_string(), + status: PlanEntryStatus::Pending, + priority: PlanEntryPriority::Low, + }, + ], + } + } + + #[gpui::test] + async fn test_run_emits_plan_event(cx: &mut TestAppContext) { + let tool = Arc::new(UpdatePlanTool); + let (event_stream, mut event_rx) = ToolCallEventStream::test(); + + let input = sample_input(); + let result = cx + .update(|cx| tool.run(ToolInput::resolved(input.clone()), event_stream, cx)) + .await + .expect("tool should succeed"); + + assert_eq!(result, "Plan updated".to_string()); + + let plan = event_rx.expect_plan().await; + assert_eq!( + plan, + acp::Plan::new(vec![ + acp::PlanEntry::new( + "Inspect the existing tool wiring", + acp::PlanEntryPriority::High, + acp::PlanEntryStatus::Completed, + ), + acp::PlanEntry::new( + "Implement the update_plan tool", + acp::PlanEntryPriority::Medium, + acp::PlanEntryStatus::InProgress, + ), + acp::PlanEntry::new( + "Add tests", + acp::PlanEntryPriority::Low, + acp::PlanEntryStatus::Pending, + ), + ]) + ); + } + + #[gpui::test] + async fn test_replay_emits_plan_event(cx: &mut TestAppContext) { + let tool = UpdatePlanTool; + let (event_stream, mut event_rx) = ToolCallEventStream::test(); + + let input = sample_input(); + + cx.update(|cx| { + tool.replay(input.clone(), "Plan updated".to_string(), event_stream, cx) + .expect("replay should succeed"); + }); + + let plan = event_rx.expect_plan().await; + assert_eq!( + plan, + acp::Plan::new(vec![ + acp::PlanEntry::new( + "Inspect the existing tool wiring", + acp::PlanEntryPriority::High, + acp::PlanEntryStatus::Completed, + ), + acp::PlanEntry::new( + "Implement the update_plan tool", + acp::PlanEntryPriority::Medium, + acp::PlanEntryStatus::InProgress, + ), + acp::PlanEntry::new( + "Add tests", + acp::PlanEntryPriority::Low, + acp::PlanEntryStatus::Pending, + ), + ]) + ); + } + + #[gpui::test] + async fn test_run_defaults_priority_to_medium(cx: &mut TestAppContext) { + let tool = Arc::new(UpdatePlanTool); + let (event_stream, mut event_rx) = ToolCallEventStream::test(); + + let input = UpdatePlanToolInput { + plan: vec![ + PlanItem { + step: "First".to_string(), + status: PlanEntryStatus::InProgress, + priority: PlanEntryPriority::default(), + }, + PlanItem { + step: "Second".to_string(), + status: PlanEntryStatus::InProgress, + priority: PlanEntryPriority::default(), + }, + ], + }; + + let result = cx + .update(|cx| tool.run(ToolInput::resolved(input), event_stream, cx)) + .await + .expect("tool should succeed"); + + assert_eq!(result, "Plan updated".to_string()); + + let plan = event_rx.expect_plan().await; + assert_eq!( + plan, + acp::Plan::new(vec![ + acp::PlanEntry::new( + "First", + acp::PlanEntryPriority::Medium, + acp::PlanEntryStatus::InProgress, + ), + acp::PlanEntry::new( + "Second", + acp::PlanEntryPriority::Medium, + acp::PlanEntryStatus::InProgress, + ), + ]) + ); + } + + #[gpui::test] + async fn test_initial_title(cx: &mut TestAppContext) { + let tool = UpdatePlanTool; + + let title = cx.update(|cx| tool.initial_title(Ok(sample_input()), cx)); + assert_eq!(title, SharedString::from("Update plan")); + + let title = + cx.update(|cx| tool.initial_title(Ok(UpdatePlanToolInput { plan: Vec::new() }), cx)); + assert_eq!(title, SharedString::from("Clear plan")); + } +} diff --git a/crates/eval/src/example.rs b/crates/eval/src/example.rs index f8171177e9ba141451390aa65a583d5094c884d2..d74df7c7f12696a94f6204fae4586c8cec36517d 100644 --- a/crates/eval/src/example.rs +++ b/crates/eval/src/example.rs @@ -328,6 +328,9 @@ impl ExampleContext { "{}Bug: Tool confirmation should not be required in eval", log_prefix ), + ThreadEvent::Plan(plan) => { + println!("{log_prefix} Got plan: {plan:?}"); + } ThreadEvent::SubagentSpawned(session) => { println!("{log_prefix} Got subagent spawn: {session:?}"); } diff --git a/crates/feature_flags/src/flags.rs b/crates/feature_flags/src/flags.rs index da91382819ec51d35b7e659b961438a70a96c3c0..985257577f53314da218934e99156a069808e999 100644 --- a/crates/feature_flags/src/flags.rs +++ b/crates/feature_flags/src/flags.rs @@ -63,6 +63,16 @@ impl FeatureFlag for StreamingEditFileToolFeatureFlag { } } +pub struct UpdatePlanToolFeatureFlag; + +impl FeatureFlag for UpdatePlanToolFeatureFlag { + const NAME: &'static str = "update-plan-tool"; + + fn enabled_for_staff() -> bool { + true + } +} + pub struct ProjectPanelUndoRedoFeatureFlag; impl FeatureFlag for ProjectPanelUndoRedoFeatureFlag { diff --git a/crates/settings_ui/src/pages/tool_permissions_setup.rs b/crates/settings_ui/src/pages/tool_permissions_setup.rs index 6697eb022c4ecf7d43a79c116105051475e98dd7..3a208d062c56a2e0617288e2212e6ba5ff2be0ed 100644 --- a/crates/settings_ui/src/pages/tool_permissions_setup.rs +++ b/crates/settings_ui/src/pages/tool_permissions_setup.rs @@ -1421,6 +1421,9 @@ mod tests { // Subagent permission checks happen at the level of individual // tool calls within the subagent, not at the spawning level. "spawn_agent", + // update_plan updates UI-visible planning state but does not use + // tool permission rules. + "update_plan", ]; let tool_info_ids: Vec<&str> = TOOLS.iter().map(|t| t.id).collect();