From 66bdb116d0023f48f413dde7ff312bd666c4db05 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Fri, 20 Feb 2026 19:51:37 +0100 Subject: [PATCH] agent: Change tool name to spawn_agent (#49741) After looking around a bit, I felt like subagent wasn't the right way to introduce this tool to the model. And with some back and forth verification + market research, we both agreed spawn_agent was a better name for what this tool is doing. It still calls the right tool if you ask for a subagent, but especially if we move these to be running in the background, spawn will be a better name anyway. Release Notes: - N/A --- assets/settings/default.json | 4 +- crates/acp_thread/src/acp_thread.rs | 2 +- crates/agent/src/tests/mod.rs | 20 ++++----- crates/agent/src/thread.rs | 4 +- crates/agent/src/tools.rs | 6 +-- .../{subagent_tool.rs => spawn_agent_tool.rs} | 42 +++++++++---------- .../src/acp/thread_view/active_thread.rs | 2 +- .../manage_profiles_modal.rs | 4 +- .../src/pages/tool_permissions_setup.rs | 2 +- 9 files changed, 43 insertions(+), 43 deletions(-) rename crates/agent/src/tools/{subagent_tool.rs => spawn_agent_tool.rs} (80%) diff --git a/assets/settings/default.json b/assets/settings/default.json index 386cc6d1e85627c72fe6b827037abbe94c5a3f06..0a57472a5f21657cab89bd3e6f64e259a4a220e6 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1040,7 +1040,7 @@ "save_file": true, "open": true, "grep": true, - "subagent": true, + "spawn_agent": true, "terminal": true, "thinking": true, "web_search": true, @@ -1060,7 +1060,7 @@ "read_file": true, "open": true, "grep": true, - "subagent": true, + "spawn_agent": true, "thinking": true, "web_search": true, }, diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index c7c598c85e5bc70669faf82da254d6eeae822b28..aace4c532d53f69954feadfaf3d49c3fe7b2da38 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -400,7 +400,7 @@ impl ToolCall { } pub fn is_subagent(&self) -> bool { - self.tool_name.as_ref().is_some_and(|s| s == "subagent") + self.tool_name.as_ref().is_some_and(|s| s == "spawn_agent") || self.subagent_session_id.is_some() } diff --git a/crates/agent/src/tests/mod.rs b/crates/agent/src/tests/mod.rs index 2673e33d0e7c8a05c204ee1c6fef6b74bac80b08..e1f45faf12f831729010dc7cbbada9596c395e5f 100644 --- a/crates/agent/src/tests/mod.rs +++ b/crates/agent/src/tests/mod.rs @@ -4230,7 +4230,7 @@ async fn test_subagent_tool_call_end_to_end(cx: &mut TestAppContext) { let send = acp_thread.update(cx, |thread, cx| thread.send_raw("Prompt", cx)); cx.run_until_parked(); model.send_last_completion_stream_text_chunk("spawning subagent"); - let subagent_tool_input = SubagentToolInput { + let subagent_tool_input = SpawnAgentToolInput { label: "label".to_string(), task: "subagent task prompt".to_string(), session_id: None, @@ -4238,7 +4238,7 @@ async fn test_subagent_tool_call_end_to_end(cx: &mut TestAppContext) { }; let subagent_tool_use = LanguageModelToolUse { id: "subagent_1".into(), - name: SubagentTool::NAME.into(), + name: SpawnAgentTool::NAME.into(), raw_input: serde_json::to_string(&subagent_tool_input).unwrap(), input: serde_json::to_value(&subagent_tool_input).unwrap(), is_input_complete: true, @@ -4381,7 +4381,7 @@ async fn test_subagent_tool_call_cancellation_during_task_prompt(cx: &mut TestAp let send = acp_thread.update(cx, |thread, cx| thread.send_raw("Prompt", cx)); cx.run_until_parked(); model.send_last_completion_stream_text_chunk("spawning subagent"); - let subagent_tool_input = SubagentToolInput { + let subagent_tool_input = SpawnAgentToolInput { label: "label".to_string(), task: "subagent task prompt".to_string(), session_id: None, @@ -4389,7 +4389,7 @@ async fn test_subagent_tool_call_cancellation_during_task_prompt(cx: &mut TestAp }; let subagent_tool_use = LanguageModelToolUse { id: "subagent_1".into(), - name: SubagentTool::NAME.into(), + name: SpawnAgentTool::NAME.into(), raw_input: serde_json::to_string(&subagent_tool_input).unwrap(), input: serde_json::to_value(&subagent_tool_input).unwrap(), is_input_complete: true, @@ -4519,7 +4519,7 @@ async fn test_subagent_tool_resume_session(cx: &mut TestAppContext) { let send = acp_thread.update(cx, |thread, cx| thread.send_raw("First prompt", cx)); cx.run_until_parked(); model.send_last_completion_stream_text_chunk("spawning subagent"); - let subagent_tool_input = SubagentToolInput { + let subagent_tool_input = SpawnAgentToolInput { label: "initial task".to_string(), task: "do the first task".to_string(), session_id: None, @@ -4527,7 +4527,7 @@ async fn test_subagent_tool_resume_session(cx: &mut TestAppContext) { }; let subagent_tool_use = LanguageModelToolUse { id: "subagent_1".into(), - name: SubagentTool::NAME.into(), + name: SpawnAgentTool::NAME.into(), raw_input: serde_json::to_string(&subagent_tool_input).unwrap(), input: serde_json::to_value(&subagent_tool_input).unwrap(), is_input_complete: true, @@ -4581,7 +4581,7 @@ async fn test_subagent_tool_resume_session(cx: &mut TestAppContext) { let send2 = acp_thread.update(cx, |thread, cx| thread.send_raw("Follow up", cx)); cx.run_until_parked(); model.send_last_completion_stream_text_chunk("resuming subagent"); - let resume_tool_input = SubagentToolInput { + let resume_tool_input = SpawnAgentToolInput { label: "follow-up task".to_string(), task: "do the follow-up task".to_string(), session_id: Some(subagent_session_id.clone()), @@ -4589,7 +4589,7 @@ async fn test_subagent_tool_resume_session(cx: &mut TestAppContext) { }; let resume_tool_use = LanguageModelToolUse { id: "subagent_2".into(), - name: SubagentTool::NAME.into(), + name: SpawnAgentTool::NAME.into(), raw_input: serde_json::to_string(&resume_tool_input).unwrap(), input: serde_json::to_value(&resume_tool_input).unwrap(), is_input_complete: true, @@ -4687,7 +4687,7 @@ async fn test_subagent_tool_is_present_when_feature_flag_enabled(cx: &mut TestAp thread.read_with(cx, |thread, _| { assert!( - thread.has_registered_tool(SubagentTool::NAME), + thread.has_registered_tool(SpawnAgentTool::NAME), "subagent tool should be present when feature flag is enabled" ); }); @@ -4780,7 +4780,7 @@ async fn test_max_subagent_depth_prevents_tool_registration(cx: &mut TestAppCont deep_subagent_thread.read_with(cx, |thread, _| { assert_eq!(thread.depth(), MAX_SUBAGENT_DEPTH); assert!( - !thread.has_registered_tool(SubagentTool::NAME), + !thread.has_registered_tool(SpawnAgentTool::NAME), "subagent tool should not be present at max depth" ); }); diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index cc8a46987d0d01a29b4b51abed21a9e415efc5a1..cd5c8f370a71148de508560704ea1dc6b5b5b924 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -2,7 +2,7 @@ use crate::{ AgentGitWorktreeInfo, ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DbLanguageModel, DbThread, DeletePathTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MovePathTool, NowTool, OpenTool, ProjectSnapshot, - ReadFileTool, RestoreFileFromDiskTool, SaveFileTool, StreamingEditFileTool, SubagentTool, + ReadFileTool, RestoreFileFromDiskTool, SaveFileTool, SpawnAgentTool, StreamingEditFileTool, SystemPromptTemplate, Template, Templates, TerminalTool, ToolPermissionDecision, WebSearchTool, decide_permission_from_settings, }; @@ -1380,7 +1380,7 @@ impl Thread { self.add_tool(WebSearchTool); if cx.has_flag::() && self.depth() < MAX_SUBAGENT_DEPTH { - self.add_tool(SubagentTool::new(cx.weak_entity(), environment)); + self.add_tool(SpawnAgentTool::new(cx.weak_entity(), environment)); } } diff --git a/crates/agent/src/tools.rs b/crates/agent/src/tools.rs index 2ffc187e953f928db9c9ee3bad9efffd8e74cecc..1962f237045c47935de90ebb231575da29d1205c 100644 --- a/crates/agent/src/tools.rs +++ b/crates/agent/src/tools.rs @@ -14,8 +14,8 @@ mod open_tool; mod read_file_tool; mod restore_file_from_disk_tool; mod save_file_tool; +mod spawn_agent_tool; mod streaming_edit_file_tool; -mod subagent_tool; mod terminal_tool; mod tool_permissions; mod web_search_tool; @@ -39,8 +39,8 @@ pub use open_tool::*; pub use read_file_tool::*; pub use restore_file_from_disk_tool::*; pub use save_file_tool::*; +pub use spawn_agent_tool::*; pub use streaming_edit_file_tool::*; -pub use subagent_tool::*; pub use terminal_tool::*; pub use tool_permissions::*; pub use web_search_tool::*; @@ -128,7 +128,7 @@ tools! { ReadFileTool, RestoreFileFromDiskTool, SaveFileTool, - SubagentTool, + SpawnAgentTool, TerminalTool, WebSearchTool, } diff --git a/crates/agent/src/tools/subagent_tool.rs b/crates/agent/src/tools/spawn_agent_tool.rs similarity index 80% rename from crates/agent/src/tools/subagent_tool.rs rename to crates/agent/src/tools/spawn_agent_tool.rs index 8212192d7bfc5119db4070aafe201f20ee4354a6..72f01c8c16e55744b8cc96566d109ee39f41fb91 100644 --- a/crates/agent/src/tools/subagent_tool.rs +++ b/crates/agent/src/tools/spawn_agent_tool.rs @@ -23,18 +23,18 @@ use crate::{AgentTool, Thread, ThreadEnvironment, ToolCallEventStream}; /// /// You will receive only the agent's final message as output. /// -/// If a response (success or error) includes a session_id, you can send a follow-up message to that session by passing the session_id back. This is useful for multi-turn conversations with a subagent, asking clarifying questions about its output, or retrying after timeouts or transient failures. +/// If a response (success or error) includes a session_id, you can send a follow-up message to that session by passing the session_id back. This is useful for multi-turn conversations with an agent, asking clarifying questions about its output, or retrying after timeouts or transient failures. /// /// Note: /// - Agents cannot use tools you don't have access to. /// - If spawning multiple agents that might write to the filesystem, provide guidance on how to avoid conflicts (e.g. assign each to different directories). #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct SubagentToolInput { +pub struct SpawnAgentToolInput { /// Short label displayed in the UI while the agent runs (e.g., "Researching alternatives") pub label: String, /// Describe the task for the agent to perform. Be specific about what you want accomplished. Include all necessary context (file paths, requirements, constraints) since the agent cannot see your conversation. pub task: String, - /// Optional session ID of an existing subagent to continue a conversation with. When provided, the task is sent as a follow-up message to that session instead of creating a new one. Use this to ask clarifying questions, request changes based on previous output, or retry after errors. + /// Optional session ID of an existing agent session to continue a conversation with. When provided, the task is sent as a follow-up message to that session instead of creating a new one. Use this to ask clarifying questions, request changes based on previous output, or retry after errors. #[serde(default)] pub session_id: Option, /// Optional maximum runtime in seconds. The purpose of this timeout is to prevent the agent from getting stuck in infinite loops, NOT to estimate task duration. Be generous if setting. If not set, the agent runs until it completes or is cancelled. @@ -44,7 +44,7 @@ pub struct SubagentToolInput { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(untagged)] -pub enum SubagentToolOutput { +pub enum SpawnAgentToolOutput { Success { session_id: acp::SessionId, output: String, @@ -57,21 +57,21 @@ pub enum SubagentToolOutput { }, } -impl From for LanguageModelToolResultContent { - fn from(output: SubagentToolOutput) -> Self { +impl From for LanguageModelToolResultContent { + fn from(output: SpawnAgentToolOutput) -> Self { serde_json::to_string(&output) - .unwrap_or_else(|e| format!("Failed to serialize subagent output: {e}")) + .unwrap_or_else(|e| format!("Failed to serialize spawn_agent output: {e}")) .into() } } -/// Tool that spawns a subagent thread to work on a task. -pub struct SubagentTool { +/// Tool that spawns an agent thread to work on a task. +pub struct SpawnAgentTool { parent_thread: WeakEntity, environment: Rc, } -impl SubagentTool { +impl SpawnAgentTool { pub fn new(parent_thread: WeakEntity, environment: Rc) -> Self { Self { parent_thread, @@ -80,11 +80,11 @@ impl SubagentTool { } } -impl AgentTool for SubagentTool { - type Input = SubagentToolInput; - type Output = SubagentToolOutput; +impl AgentTool for SpawnAgentTool { + type Input = SpawnAgentToolInput; + type Output = SpawnAgentToolOutput; - const NAME: &'static str = "subagent"; + const NAME: &'static str = "spawn_agent"; fn kind() -> acp::ToolKind { acp::ToolKind::Other @@ -97,7 +97,7 @@ impl AgentTool for SubagentTool { ) -> SharedString { input .map(|i| i.label.into()) - .unwrap_or_else(|_| "Subagent".into()) + .unwrap_or_else(|_| "Spawning agent".into()) } fn run( @@ -107,7 +107,7 @@ impl AgentTool for SubagentTool { cx: &mut App, ) -> Task> { let Some(parent_thread_entity) = self.parent_thread.upgrade() else { - return Task::ready(Err(SubagentToolOutput::Error { + return Task::ready(Err(SpawnAgentToolOutput::Error { session_id: None, error: "Parent thread no longer exists".to_string(), })); @@ -133,7 +133,7 @@ impl AgentTool for SubagentTool { let subagent = match subagent { Ok(subagent) => subagent, Err(err) => { - return Task::ready(Err(SubagentToolOutput::Error { + return Task::ready(Err(SpawnAgentToolOutput::Error { session_id: None, error: err.to_string(), })); @@ -153,11 +153,11 @@ impl AgentTool for SubagentTool { subagent .wait_for_output(cx) .await - .map_err(|e| SubagentToolOutput::Error { + .map_err(|e| SpawnAgentToolOutput::Error { session_id: Some(subagent_session_id.clone()), error: e.to_string(), })?; - Ok(SubagentToolOutput::Success { + Ok(SpawnAgentToolOutput::Success { session_id: subagent_session_id, output, }) @@ -172,8 +172,8 @@ impl AgentTool for SubagentTool { _cx: &mut App, ) -> Result<()> { let session_id = match &output { - SubagentToolOutput::Success { session_id, .. } => Some(session_id), - SubagentToolOutput::Error { session_id, .. } => session_id.as_ref(), + SpawnAgentToolOutput::Success { session_id, .. } => Some(session_id), + SpawnAgentToolOutput::Error { session_id, .. } => session_id.as_ref(), }; if let Some(session_id) = session_id { diff --git a/crates/agent_ui/src/acp/thread_view/active_thread.rs b/crates/agent_ui/src/acp/thread_view/active_thread.rs index 2630d3ffd6c41891c0b56501fb36a027f141aa91..6f13caaeb25dba5c086f279920620637c42d24a5 100644 --- a/crates/agent_ui/src/acp/thread_view/active_thread.rs +++ b/crates/agent_ui/src/acp/thread_view/active_thread.rs @@ -6050,7 +6050,7 @@ impl AcpThreadView { if is_canceled_or_failed { "Subagent Canceled" } else { - "Creating Subagent…" + "Spawning agent…" } .into() }); diff --git a/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs b/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs index 8bb08404f9fbc4cadf78fabc909122a6118c48ec..f46588c79033d965cbee0aaeb2624e7ae0756af6 100644 --- a/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs +++ b/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs @@ -2,7 +2,7 @@ mod profile_modal_header; use std::sync::Arc; -use agent::{AgentTool, ContextServerRegistry, SubagentTool}; +use agent::{AgentTool, ContextServerRegistry, SpawnAgentTool}; use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, builtin_profiles}; use editor::Editor; use feature_flags::{FeatureFlagAppExt as _, SubagentsFeatureFlag}; @@ -364,7 +364,7 @@ impl ManageProfilesModal { agent::tool_supports_provider(name, provider) }); let enabled_by_feature_flag = - *name != SubagentTool::NAME || cx.has_flag::(); + *name != SpawnAgentTool::NAME || cx.has_flag::(); supported_by_provider && enabled_by_feature_flag }) diff --git a/crates/settings_ui/src/pages/tool_permissions_setup.rs b/crates/settings_ui/src/pages/tool_permissions_setup.rs index 848a42b5866fb8115395a54bce472c4aaf8d2eec..c1c978efbb3da5dc57c8d40a45370a908698bd40 100644 --- a/crates/settings_ui/src/pages/tool_permissions_setup.rs +++ b/crates/settings_ui/src/pages/tool_permissions_setup.rs @@ -1412,7 +1412,7 @@ mod tests { "streaming_edit_file", // Subagent permission checks happen at the level of individual // tool calls within the subagent, not at the spawning level. - "subagent", + "spawn_agent", ]; let tool_info_ids: Vec<&str> = TOOLS.iter().map(|t| t.id).collect();