From c6356c5149028365213cf9b4a8af1f333be91f5f Mon Sep 17 00:00:00 2001 From: Tyler Longwell Date: Wed, 17 Dec 2025 11:02:29 -0500 Subject: [PATCH 01/17] initial work on subagent platform extension --- crates/goose/src/agents/agent.rs | 94 +++----- .../src/agents/code_execution_extension.rs | 14 +- crates/goose/src/agents/extension.rs | 21 +- crates/goose/src/agents/extension_manager.rs | 7 + crates/goose/src/agents/mod.rs | 1 + crates/goose/src/agents/subagent_client.rs | 203 ++++++++++++++++++ .../goose/src/agents/subagent_task_config.rs | 15 ++ crates/goose/src/execution/manager.rs | 3 + crates/goose/tests/agent.rs | 3 + 9 files changed, 287 insertions(+), 74 deletions(-) create mode 100644 crates/goose/src/agents/subagent_client.rs diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index 74a4942a1a69..616416becf14 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -20,10 +20,7 @@ use crate::agents::platform_tools::PLATFORM_MANAGE_SCHEDULE_TOOL_NAME; use crate::agents::prompt_manager::PromptManager; use crate::agents::retry::{RetryManager, RetryResult}; use crate::agents::router_tools::ROUTER_LLM_SEARCH_TOOL_NAME; -use crate::agents::subagent_task_config::TaskConfig; -use crate::agents::subagent_tool::{ - create_subagent_tool, handle_subagent_tool, SUBAGENT_TOOL_NAME, -}; +use crate::agents::subagent_client; use crate::agents::tool_route_manager::ToolRouteManager; use crate::agents::tool_router_index_manager::ToolRouterIndexManager; use crate::agents::types::SessionConfig; @@ -85,7 +82,7 @@ pub struct Agent { pub(super) provider: SharedProvider, pub extension_manager: Arc, - pub(super) sub_recipes: Mutex>, + pub(super) sub_recipes: Arc>>, pub(super) final_output_tool: Arc>>, pub(super) frontend_tools: Mutex>, pub(super) frontend_instructions: Mutex>, @@ -160,7 +157,7 @@ impl Agent { Self { provider: provider.clone(), extension_manager: Arc::new(ExtensionManager::new(provider.clone())), - sub_recipes: Mutex::new(HashMap::new()), + sub_recipes: Arc::new(tokio::sync::RwLock::new(HashMap::new())), final_output_tool: Arc::new(Mutex::new(None)), frontend_tools: Mutex::new(HashMap::new()), frontend_instructions: Mutex::new(None), @@ -267,6 +264,8 @@ impl Agent { let initial_messages = conversation.messages().clone(); let config = Config::global(); + let _ = self.enable_subagent_extension().await; + let (tools, toolshim_tools, system_prompt) = self.prepare_tools_and_prompt(working_dir).await?; let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto); @@ -399,8 +398,12 @@ impl Agent { self.extend_system_prompt(final_output_system_prompt).await; } + pub fn sub_recipes(&self) -> Arc>> { + self.sub_recipes.clone() + } + pub async fn add_sub_recipes(&self, sub_recipes_to_add: Vec) { - let mut sub_recipes = self.sub_recipes.lock().await; + let mut sub_recipes = self.sub_recipes.write().await; for sr in sub_recipes_to_add { sub_recipes.insert(sr.name.clone(), sr); } @@ -430,20 +433,8 @@ impl Agent { tool_call: CallToolRequestParam, request_id: String, cancellation_token: Option, - session: &Session, + _session: &Session, ) -> (String, Result) { - // Prevent subagents from creating other subagents - if session.session_type == SessionType::SubAgent && tool_call.name == SUBAGENT_TOOL_NAME { - return ( - request_id, - Err(ErrorData::new( - ErrorCode::INVALID_REQUEST, - "Subagents cannot create other subagents".to_string(), - None, - )), - ); - } - if tool_call.name == PLATFORM_MANAGE_SCHEDULE_TOOL_NAME { let arguments = tool_call .arguments @@ -478,40 +469,7 @@ impl Agent { } debug!("WAITING_TOOL_START: {}", tool_call.name); - let result: ToolCallResult = if tool_call.name == SUBAGENT_TOOL_NAME { - let provider = match self.provider().await { - Ok(p) => p, - Err(_) => { - return ( - request_id, - Err(ErrorData::new( - ErrorCode::INTERNAL_ERROR, - "Provider is required".to_string(), - None, - )), - ); - } - }; - - let extensions = self.get_extension_configs().await; - let task_config = - TaskConfig::new(provider, &session.id, &session.working_dir, extensions); - let sub_recipes = self.sub_recipes.lock().await.clone(); - - let arguments = tool_call - .arguments - .clone() - .map(Value::Object) - .unwrap_or(Value::Object(serde_json::Map::new())); - - handle_subagent_tool( - arguments, - task_config, - sub_recipes, - session.working_dir.clone(), - cancellation_token, - ) - } else if self.is_frontend_tool(&tool_call.name).await { + let result: ToolCallResult = if self.is_frontend_tool(&tool_call.name).await { // For frontend tools, return an error indicating we need frontend execution ToolCallResult::from(Err(ErrorData::new( ErrorCode::INTERNAL_ERROR, @@ -678,7 +636,6 @@ impl Agent { .await .unwrap_or_default(); - let subagents_enabled = self.subagents_enabled().await; if extension_name.is_none() || extension_name.as_deref() == Some("platform") { prefixed_tools.push(platform_tools::manage_schedule_tool()); } @@ -687,17 +644,32 @@ impl Agent { if let Some(final_output_tool) = self.final_output_tool.lock().await.as_ref() { prefixed_tools.push(final_output_tool.tool()); } - - if subagents_enabled { - let sub_recipes = self.sub_recipes.lock().await; - let sub_recipes_vec: Vec<_> = sub_recipes.values().cloned().collect(); - prefixed_tools.push(create_subagent_tool(&sub_recipes_vec)); - } } prefixed_tools } + pub async fn enable_subagent_extension(&self) -> ExtensionResult<()> { + if !self.subagents_enabled().await { + return Ok(()); + } + if self + .extension_manager + .is_extension_enabled(subagent_client::EXTENSION_NAME) + .await + { + return Ok(()); + } + self.extension_manager + .add_extension(ExtensionConfig::Platform { + name: subagent_client::EXTENSION_NAME.to_string(), + description: "Delegate tasks to independent subagents".to_string(), + bundled: Some(true), + available_tools: vec![], + }) + .await + } + pub async fn list_tools_for_router(&self) -> Vec { self.tool_route_manager .list_tools_for_router(&self.extension_manager) diff --git a/crates/goose/src/agents/code_execution_extension.rs b/crates/goose/src/agents/code_execution_extension.rs index e9903c18d232..7aec94d00f95 100644 --- a/crates/goose/src/agents/code_execution_extension.rs +++ b/crates/goose/src/agents/code_execution_extension.rs @@ -758,12 +758,7 @@ mod tests { #[tokio::test] async fn test_execute_code_simple() { - let context = PlatformExtensionContext { - session_id: None, - extension_manager: None, - tool_route_manager: None, - }; - let client = CodeExecutionClient::new(context).unwrap(); + let client = CodeExecutionClient::new(PlatformExtensionContext::default()).unwrap(); let mut args = JsonObject::new(); args.insert("code".to_string(), Value::String("2 + 2".to_string())); @@ -783,12 +778,7 @@ mod tests { #[tokio::test] async fn test_read_module_not_found() { - let context = PlatformExtensionContext { - session_id: None, - extension_manager: None, - tool_route_manager: None, - }; - let client = CodeExecutionClient::new(context).unwrap(); + let client = CodeExecutionClient::new(PlatformExtensionContext::default()).unwrap(); let mut args = JsonObject::new(); args.insert("path".to_string(), Value::String("nonexistent".to_string())); diff --git a/crates/goose/src/agents/extension.rs b/crates/goose/src/agents/extension.rs index 3287a40b06e2..7e409f20b89f 100644 --- a/crates/goose/src/agents/extension.rs +++ b/crates/goose/src/agents/extension.rs @@ -2,8 +2,14 @@ use crate::agents::chatrecall_extension; use crate::agents::code_execution_extension; use crate::agents::extension_manager_extension; use crate::agents::skills_extension; +use crate::agents::subagent_client; use crate::agents::todo_extension; +use crate::recipe::SubRecipe; +use crate::session::session_manager::SessionType; use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::sync::RwLock; use crate::agents::mcp_client::McpClientTrait; use crate::config; @@ -100,17 +106,30 @@ pub static PLATFORM_EXTENSIONS: Lazy }, ); + map.insert( + subagent_client::EXTENSION_NAME, + PlatformExtensionDef { + name: subagent_client::EXTENSION_NAME, + description: "Delegate tasks to independent subagents", + default_enabled: false, + client_factory: |ctx| Box::new(subagent_client::SubagentClient::new(ctx).unwrap()), + }, + ); + map }, ); -#[derive(Clone)] +#[derive(Clone, Default)] pub struct PlatformExtensionContext { pub session_id: Option, pub extension_manager: Option>, pub tool_route_manager: Option>, + pub session_type: Option, + pub working_dir: Option, + pub sub_recipes: Option>>>, } #[derive(Debug, Clone)] diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index abee7d7a2443..085daa0bfb36 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -262,6 +262,9 @@ impl ExtensionManager { session_id: None, extension_manager: None, tool_route_manager: None, + session_type: None, + working_dir: None, + sub_recipes: None, }), provider, } @@ -280,6 +283,10 @@ impl ExtensionManager { self.context.lock().await.clone() } + pub async fn get_provider(&self) -> Option> { + self.provider.lock().await.clone() + } + pub async fn supports_resources(&self) -> bool { self.extensions .lock() diff --git a/crates/goose/src/agents/mod.rs b/crates/goose/src/agents/mod.rs index ceddd2bd1f65..14c77730f45e 100644 --- a/crates/goose/src/agents/mod.rs +++ b/crates/goose/src/agents/mod.rs @@ -18,6 +18,7 @@ mod router_tool_selector; mod router_tools; mod schedule_tool; pub(crate) mod skills_extension; +pub(crate) mod subagent_client; pub mod subagent_execution_tool; pub mod subagent_handler; mod subagent_task_config; diff --git a/crates/goose/src/agents/subagent_client.rs b/crates/goose/src/agents/subagent_client.rs new file mode 100644 index 000000000000..256e92fff34d --- /dev/null +++ b/crates/goose/src/agents/subagent_client.rs @@ -0,0 +1,203 @@ +use crate::agents::extension::PlatformExtensionContext; +use crate::agents::mcp_client::{Error, McpClientTrait}; +use crate::agents::subagent_task_config::TaskConfig; +use crate::agents::subagent_tool::{ + create_subagent_tool, handle_subagent_tool, SUBAGENT_TOOL_NAME, +}; +use crate::config::get_enabled_extensions; +use crate::session::session_manager::SessionType; +use anyhow::Result; +use async_trait::async_trait; +use rmcp::model::{ + CallToolResult, Content, GetPromptResult, Implementation, InitializeResult, JsonObject, + ListPromptsResult, ListResourcesResult, ListToolsResult, ProtocolVersion, ReadResourceResult, + ServerCapabilities, ServerNotification, Tool, ToolsCapability, +}; +use serde_json::Value; +use std::path::PathBuf; +use tokio::sync::mpsc; +use tokio_util::sync::CancellationToken; + +pub const EXTENSION_NAME: &str = "subagent"; + +pub struct SubagentClient { + context: PlatformExtensionContext, + info: InitializeResult, +} + +impl SubagentClient { + pub fn new(context: PlatformExtensionContext) -> Result { + Ok(Self { + context, + info: InitializeResult { + protocol_version: ProtocolVersion::V_2025_03_26, + capabilities: ServerCapabilities { + tools: Some(ToolsCapability { + list_changed: Some(false), + }), + resources: None, + prompts: None, + completions: None, + experimental: None, + logging: None, + }, + server_info: Implementation { + name: EXTENSION_NAME.to_string(), + title: Some("Subagent".to_string()), + version: env!("CARGO_PKG_VERSION").to_string(), + icons: None, + website_url: None, + }, + instructions: Some( + "Delegate tasks to independent subagents for parallel or focused work." + .to_string(), + ), + }, + }) + } + + fn is_subagent_session(&self) -> bool { + matches!(self.context.session_type, Some(SessionType::SubAgent)) + } + + async fn get_provider(&self) -> Option> { + let em = self.context.extension_manager.as_ref()?.upgrade()?; + em.get_provider().await + } + + async fn get_sub_recipes(&self) -> std::collections::HashMap { + match &self.context.sub_recipes { + Some(recipes) => recipes.read().await.clone(), + None => std::collections::HashMap::new(), + } + } + + fn get_working_dir(&self) -> PathBuf { + self.context + .working_dir + .clone() + .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))) + } + + async fn build_tool(&self) -> Tool { + let sub_recipes = self.get_sub_recipes().await; + let sub_recipes_vec: Vec<_> = sub_recipes.values().cloned().collect(); + create_subagent_tool(&sub_recipes_vec) + } +} + +#[async_trait] +impl McpClientTrait for SubagentClient { + async fn list_resources( + &self, + _next_cursor: Option, + _cancellation_token: CancellationToken, + ) -> Result { + Err(Error::TransportClosed) + } + + async fn read_resource( + &self, + _uri: &str, + _cancellation_token: CancellationToken, + ) -> Result { + Err(Error::TransportClosed) + } + + async fn list_tools( + &self, + _next_cursor: Option, + _cancellation_token: CancellationToken, + ) -> Result { + if self.is_subagent_session() { + return Ok(ListToolsResult { + tools: Vec::new(), + next_cursor: None, + }); + } + + Ok(ListToolsResult { + tools: vec![self.build_tool().await], + next_cursor: None, + }) + } + + async fn call_tool( + &self, + name: &str, + arguments: Option, + cancellation_token: CancellationToken, + ) -> Result { + if name != SUBAGENT_TOOL_NAME { + return Ok(CallToolResult::error(vec![Content::text(format!( + "Unknown tool: {}", + name + ))])); + } + + if self.is_subagent_session() { + return Ok(CallToolResult::error(vec![Content::text( + "Subagents cannot spawn other subagents", + )])); + } + + let Some(provider) = self.get_provider().await else { + return Ok(CallToolResult::error(vec![Content::text( + "No provider configured", + )])); + }; + + let extensions = get_enabled_extensions(); + let working_dir = self.get_working_dir(); + let sub_recipes = self.get_sub_recipes().await; + let task_config = TaskConfig::new_minimal(provider, extensions); + + let arguments_value = arguments + .map(Value::Object) + .unwrap_or(Value::Object(serde_json::Map::new())); + + let result = handle_subagent_tool( + arguments_value, + task_config, + sub_recipes, + working_dir, + Some(cancellation_token), + ); + + match result.result.await { + Ok(call_result) => Ok(call_result), + Err(error_data) => Ok(CallToolResult::error(vec![Content::text( + error_data.message.to_string(), + )])), + } + } + + async fn list_prompts( + &self, + _next_cursor: Option, + _cancellation_token: CancellationToken, + ) -> Result { + Err(Error::TransportClosed) + } + + async fn get_prompt( + &self, + _name: &str, + _arguments: Value, + _cancellation_token: CancellationToken, + ) -> Result { + Err(Error::TransportClosed) + } + + async fn subscribe(&self) -> mpsc::Receiver { + mpsc::channel(1).1 + } + + fn get_info(&self) -> Option<&InitializeResult> { + Some(&self.info) + } + + async fn get_moim(&self) -> Option { + None + } +} diff --git a/crates/goose/src/agents/subagent_task_config.rs b/crates/goose/src/agents/subagent_task_config.rs index 01c955d0c01c..6437500600f9 100644 --- a/crates/goose/src/agents/subagent_task_config.rs +++ b/crates/goose/src/agents/subagent_task_config.rs @@ -53,4 +53,19 @@ impl TaskConfig { ), } } + + pub fn new_minimal(provider: Arc, extensions: Vec) -> Self { + Self { + provider, + parent_session_id: String::new(), + parent_working_dir: PathBuf::new(), + extensions, + max_turns: Some( + env::var(GOOSE_SUBAGENT_MAX_TURNS_ENV_VAR) + .ok() + .and_then(|val| val.parse::().ok()) + .unwrap_or(DEFAULT_SUBAGENT_MAX_TURNS), + ), + } + } } diff --git a/crates/goose/src/execution/manager.rs b/crates/goose/src/execution/manager.rs index cef71c829a1d..368ca6ed8e97 100644 --- a/crates/goose/src/execution/manager.rs +++ b/crates/goose/src/execution/manager.rs @@ -88,6 +88,9 @@ impl AgentManager { session_id: Some(session_id.clone()), extension_manager: Some(Arc::downgrade(&agent.extension_manager)), tool_route_manager: Some(Arc::downgrade(&agent.tool_route_manager)), + session_type: None, + working_dir: None, + sub_recipes: Some(agent.sub_recipes()), }) .await; if let Some(provider) = &*self.default_provider.read().await { diff --git a/crates/goose/tests/agent.rs b/crates/goose/tests/agent.rs index 85485b5c9848..ca68a5c7062a 100644 --- a/crates/goose/tests/agent.rs +++ b/crates/goose/tests/agent.rs @@ -474,6 +474,9 @@ mod tests { session_id: Some("test_session".to_string()), extension_manager: Some(Arc::downgrade(&agent.extension_manager)), tool_route_manager: Some(Arc::downgrade(&agent.tool_route_manager)), + session_type: None, + working_dir: None, + sub_recipes: Some(agent.sub_recipes()), }) .await; From 23ab12dbafbc76a2ebddb463ac5bf96bad3d6991 Mon Sep 17 00:00:00 2001 From: Tyler Longwell Date: Wed, 17 Dec 2025 11:21:06 -0500 Subject: [PATCH 02/17] subagent cleanup --- crates/goose/src/agents/agent.rs | 4 ++- crates/goose/src/agents/extension.rs | 2 -- crates/goose/src/agents/extension_manager.rs | 1 - crates/goose/src/agents/subagent_client.rs | 35 ++++++++----------- .../goose/src/agents/subagent_task_config.rs | 33 +---------------- crates/goose/src/execution/manager.rs | 1 - crates/goose/tests/agent.rs | 1 - 7 files changed, 19 insertions(+), 58 deletions(-) diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index 616416becf14..4e6afd594310 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -264,7 +264,9 @@ impl Agent { let initial_messages = conversation.messages().clone(); let config = Config::global(); - let _ = self.enable_subagent_extension().await; + if let Err(e) = self.enable_subagent_extension().await { + warn!("Failed to enable subagent extension: {}", e); + } let (tools, toolshim_tools, system_prompt) = self.prepare_tools_and_prompt(working_dir).await?; diff --git a/crates/goose/src/agents/extension.rs b/crates/goose/src/agents/extension.rs index 7e409f20b89f..83b8a2c3ddc7 100644 --- a/crates/goose/src/agents/extension.rs +++ b/crates/goose/src/agents/extension.rs @@ -5,7 +5,6 @@ use crate::agents::skills_extension; use crate::agents::subagent_client; use crate::agents::todo_extension; use crate::recipe::SubRecipe; -use crate::session::session_manager::SessionType; use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; @@ -127,7 +126,6 @@ pub struct PlatformExtensionContext { Option>, pub tool_route_manager: Option>, - pub session_type: Option, pub working_dir: Option, pub sub_recipes: Option>>>, } diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index 085daa0bfb36..e061b395d80c 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -262,7 +262,6 @@ impl ExtensionManager { session_id: None, extension_manager: None, tool_route_manager: None, - session_type: None, working_dir: None, sub_recipes: None, }), diff --git a/crates/goose/src/agents/subagent_client.rs b/crates/goose/src/agents/subagent_client.rs index 256e92fff34d..278a23e6b575 100644 --- a/crates/goose/src/agents/subagent_client.rs +++ b/crates/goose/src/agents/subagent_client.rs @@ -5,7 +5,6 @@ use crate::agents::subagent_tool::{ create_subagent_tool, handle_subagent_tool, SUBAGENT_TOOL_NAME, }; use crate::config::get_enabled_extensions; -use crate::session::session_manager::SessionType; use anyhow::Result; use async_trait::async_trait; use rmcp::model::{ @@ -56,15 +55,24 @@ impl SubagentClient { }) } - fn is_subagent_session(&self) -> bool { - matches!(self.context.session_type, Some(SessionType::SubAgent)) - } - async fn get_provider(&self) -> Option> { let em = self.context.extension_manager.as_ref()?.upgrade()?; em.get_provider().await } + async fn get_extensions(&self) -> Vec { + if let Some(em) = self + .context + .extension_manager + .as_ref() + .and_then(|w| w.upgrade()) + { + em.get_extension_configs().await + } else { + get_enabled_extensions() + } + } + async fn get_sub_recipes(&self) -> std::collections::HashMap { match &self.context.sub_recipes { Some(recipes) => recipes.read().await.clone(), @@ -109,13 +117,6 @@ impl McpClientTrait for SubagentClient { _next_cursor: Option, _cancellation_token: CancellationToken, ) -> Result { - if self.is_subagent_session() { - return Ok(ListToolsResult { - tools: Vec::new(), - next_cursor: None, - }); - } - Ok(ListToolsResult { tools: vec![self.build_tool().await], next_cursor: None, @@ -135,22 +136,16 @@ impl McpClientTrait for SubagentClient { ))])); } - if self.is_subagent_session() { - return Ok(CallToolResult::error(vec![Content::text( - "Subagents cannot spawn other subagents", - )])); - } - let Some(provider) = self.get_provider().await else { return Ok(CallToolResult::error(vec![Content::text( "No provider configured", )])); }; - let extensions = get_enabled_extensions(); + let extensions = self.get_extensions().await; let working_dir = self.get_working_dir(); let sub_recipes = self.get_sub_recipes().await; - let task_config = TaskConfig::new_minimal(provider, extensions); + let task_config = TaskConfig::new(provider, extensions); let arguments_value = arguments .map(Value::Object) diff --git a/crates/goose/src/agents/subagent_task_config.rs b/crates/goose/src/agents/subagent_task_config.rs index 6437500600f9..7cdb54145829 100644 --- a/crates/goose/src/agents/subagent_task_config.rs +++ b/crates/goose/src/agents/subagent_task_config.rs @@ -2,21 +2,14 @@ use crate::agents::ExtensionConfig; use crate::providers::base::Provider; use std::env; use std::fmt; -use std::path::{Path, PathBuf}; use std::sync::Arc; -/// Default maximum number of turns for task execution pub const DEFAULT_SUBAGENT_MAX_TURNS: usize = 25; - -/// Environment variable name for configuring max turns pub const GOOSE_SUBAGENT_MAX_TURNS_ENV_VAR: &str = "GOOSE_SUBAGENT_MAX_TURNS"; -/// Configuration for task execution with all necessary dependencies #[derive(Clone)] pub struct TaskConfig { pub provider: Arc, - pub parent_session_id: String, - pub parent_working_dir: PathBuf, pub extensions: Vec, pub max_turns: Option, } @@ -25,8 +18,6 @@ impl fmt::Debug for TaskConfig { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("TaskConfig") .field("provider", &"") - .field("parent_session_id", &self.parent_session_id) - .field("parent_working_dir", &self.parent_working_dir) .field("max_turns", &self.max_turns) .field("extensions", &self.extensions) .finish() @@ -34,31 +25,9 @@ impl fmt::Debug for TaskConfig { } impl TaskConfig { - pub fn new( - provider: Arc, - parent_session_id: &str, - parent_working_dir: &Path, - extensions: Vec, - ) -> Self { - Self { - provider, - parent_session_id: parent_session_id.to_owned(), - parent_working_dir: parent_working_dir.to_owned(), - extensions, - max_turns: Some( - env::var(GOOSE_SUBAGENT_MAX_TURNS_ENV_VAR) - .ok() - .and_then(|val| val.parse::().ok()) - .unwrap_or(DEFAULT_SUBAGENT_MAX_TURNS), - ), - } - } - - pub fn new_minimal(provider: Arc, extensions: Vec) -> Self { + pub fn new(provider: Arc, extensions: Vec) -> Self { Self { provider, - parent_session_id: String::new(), - parent_working_dir: PathBuf::new(), extensions, max_turns: Some( env::var(GOOSE_SUBAGENT_MAX_TURNS_ENV_VAR) diff --git a/crates/goose/src/execution/manager.rs b/crates/goose/src/execution/manager.rs index 368ca6ed8e97..08ea38aad2eb 100644 --- a/crates/goose/src/execution/manager.rs +++ b/crates/goose/src/execution/manager.rs @@ -88,7 +88,6 @@ impl AgentManager { session_id: Some(session_id.clone()), extension_manager: Some(Arc::downgrade(&agent.extension_manager)), tool_route_manager: Some(Arc::downgrade(&agent.tool_route_manager)), - session_type: None, working_dir: None, sub_recipes: Some(agent.sub_recipes()), }) diff --git a/crates/goose/tests/agent.rs b/crates/goose/tests/agent.rs index ca68a5c7062a..285c1dc7d92d 100644 --- a/crates/goose/tests/agent.rs +++ b/crates/goose/tests/agent.rs @@ -474,7 +474,6 @@ mod tests { session_id: Some("test_session".to_string()), extension_manager: Some(Arc::downgrade(&agent.extension_manager)), tool_route_manager: Some(Arc::downgrade(&agent.tool_route_manager)), - session_type: None, working_dir: None, sub_recipes: Some(agent.sub_recipes()), }) From a6a2886ef3c122896725a7a684bb186763fc30c1 Mon Sep 17 00:00:00 2001 From: Tyler Longwell Date: Wed, 17 Dec 2025 15:44:49 -0500 Subject: [PATCH 03/17] parallelism is hard --- crates/goose-cli/src/session/builder.rs | 2 + crates/goose/src/agents/extension_manager.rs | 34 ++++++++--------- crates/goose/src/agents/mcp_client.rs | 20 ++++++++++ crates/goose/src/agents/subagent_client.rs | 39 ++++++++++++-------- 4 files changed, 62 insertions(+), 33 deletions(-) diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs index f7771af20d64..4353c4e25c23 100644 --- a/crates/goose-cli/src/session/builder.rs +++ b/crates/goose-cli/src/session/builder.rs @@ -387,6 +387,8 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { session_id: Some(session_id.clone()), extension_manager: Some(Arc::downgrade(&agent.extension_manager)), tool_route_manager: Some(Arc::downgrade(&agent.tool_route_manager)), + working_dir: None, + sub_recipes: Some(agent.sub_recipes()), }) .await; diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index e061b395d80c..8d27efa3a05b 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -1,8 +1,8 @@ use anyhow::Result; use axum::http::{HeaderMap, HeaderName}; use chrono::{DateTime, Utc}; +use futures::future; use futures::stream::{FuturesUnordered, StreamExt}; -use futures::{future, FutureExt}; use rmcp::service::{ClientInitializeError, ServiceError}; use rmcp::transport::streamable_http_client::{ AuthRequiredError, StreamableHttpClientTransportConfig, StreamableHttpError, @@ -1086,26 +1086,24 @@ impl ExtensionManager { } let arguments = tool_call.arguments.clone(); - let client = client.clone(); let notifications_receiver = client.lock().await.subscribe().await; - let fut = async move { - let client_guard = client.lock().await; - client_guard - .call_tool(&tool_name, arguments, cancellation_token) - .await - .map_err(|e| match e { - ServiceError::McpError(error_data) => error_data, - _ => { - ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), e.maybe_to_value()) - } - }) - }; + let mut result = client + .lock() + .await + .call_tool_deferred(&tool_name, arguments, cancellation_token) + .await + .map_err(|e| match e { + ServiceError::McpError(error_data) => error_data, + _ => ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), e.maybe_to_value()), + })?; - Ok(ToolCallResult { - result: Box::new(fut.boxed()), - notification_stream: Some(Box::new(ReceiverStream::new(notifications_receiver))), - }) + if result.notification_stream.is_none() { + result.notification_stream = + Some(Box::new(ReceiverStream::new(notifications_receiver))); + } + + Ok(result) } pub async fn list_prompts_from_extension( diff --git a/crates/goose/src/agents/mcp_client.rs b/crates/goose/src/agents/mcp_client.rs index 677dabf9149f..dd24ed63ee48 100644 --- a/crates/goose/src/agents/mcp_client.rs +++ b/crates/goose/src/agents/mcp_client.rs @@ -1,4 +1,5 @@ use crate::action_required_manager::ActionRequiredManager; +use crate::agents::tool_execution::ToolCallResult; use crate::agents::types::SharedProvider; use crate::session_context::SESSION_ID_HEADER; use rmcp::model::{ @@ -64,6 +65,25 @@ pub trait McpClientTrait: Send + Sync { cancel_token: CancellationToken, ) -> Result; + /// Returns a deferred tool call result that can be awaited later. + /// This enables parallel execution of multiple tool calls. + /// Default implementation wraps call_tool for backward compatibility. + async fn call_tool_deferred( + &self, + name: &str, + arguments: Option, + cancel_token: CancellationToken, + ) -> Result { + let result = self.call_tool(name, arguments, cancel_token).await; + Ok(ToolCallResult { + result: Box::new(futures::future::ready(result.map_err(|e| match e { + ServiceError::McpError(error_data) => error_data, + _ => ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None), + }))), + notification_stream: None, + }) + } + async fn list_prompts( &self, next_cursor: Option, diff --git a/crates/goose/src/agents/subagent_client.rs b/crates/goose/src/agents/subagent_client.rs index 278a23e6b575..512f8d9e62b3 100644 --- a/crates/goose/src/agents/subagent_client.rs +++ b/crates/goose/src/agents/subagent_client.rs @@ -4,6 +4,7 @@ use crate::agents::subagent_task_config::TaskConfig; use crate::agents::subagent_tool::{ create_subagent_tool, handle_subagent_tool, SUBAGENT_TOOL_NAME, }; +use crate::agents::tool_execution::ToolCallResult; use crate::config::get_enabled_extensions; use anyhow::Result; use async_trait::async_trait; @@ -126,8 +127,8 @@ impl McpClientTrait for SubagentClient { async fn call_tool( &self, name: &str, - arguments: Option, - cancellation_token: CancellationToken, + _arguments: Option, + _cancellation_token: CancellationToken, ) -> Result { if name != SUBAGENT_TOOL_NAME { return Ok(CallToolResult::error(vec![Content::text(format!( @@ -135,36 +136,44 @@ impl McpClientTrait for SubagentClient { name ))])); } + Ok(CallToolResult::error(vec![Content::text( + "Subagent tool must be called via call_tool_deferred", + )])) + } + + async fn call_tool_deferred( + &self, + name: &str, + arguments: Option, + cancellation_token: CancellationToken, + ) -> Result { + if name != SUBAGENT_TOOL_NAME { + return Ok(ToolCallResult::from(Ok(CallToolResult::error(vec![ + Content::text(format!("Unknown tool: {}", name)), + ])))); + } let Some(provider) = self.get_provider().await else { - return Ok(CallToolResult::error(vec![Content::text( - "No provider configured", - )])); + return Ok(ToolCallResult::from(Ok(CallToolResult::error(vec![ + Content::text("No provider configured"), + ])))); }; let extensions = self.get_extensions().await; let working_dir = self.get_working_dir(); let sub_recipes = self.get_sub_recipes().await; let task_config = TaskConfig::new(provider, extensions); - let arguments_value = arguments .map(Value::Object) .unwrap_or(Value::Object(serde_json::Map::new())); - let result = handle_subagent_tool( + Ok(handle_subagent_tool( arguments_value, task_config, sub_recipes, working_dir, Some(cancellation_token), - ); - - match result.result.await { - Ok(call_result) => Ok(call_result), - Err(error_data) => Ok(CallToolResult::error(vec![Content::text( - error_data.message.to_string(), - )])), - } + )) } async fn list_prompts( From 23d9847494e34dadd3393e6d001562ac8ed922e9 Mon Sep 17 00:00:00 2001 From: Tyler Longwell Date: Wed, 17 Dec 2025 16:17:59 -0500 Subject: [PATCH 04/17] prevent recursion in subagents --- crates/goose/src/agents/mcp_client.rs | 3 --- crates/goose/src/agents/subagent_client.rs | 12 ++++++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/goose/src/agents/mcp_client.rs b/crates/goose/src/agents/mcp_client.rs index dd24ed63ee48..e4d21f2d5714 100644 --- a/crates/goose/src/agents/mcp_client.rs +++ b/crates/goose/src/agents/mcp_client.rs @@ -65,9 +65,6 @@ pub trait McpClientTrait: Send + Sync { cancel_token: CancellationToken, ) -> Result; - /// Returns a deferred tool call result that can be awaited later. - /// This enables parallel execution of multiple tool calls. - /// Default implementation wraps call_tool for backward compatibility. async fn call_tool_deferred( &self, name: &str, diff --git a/crates/goose/src/agents/subagent_client.rs b/crates/goose/src/agents/subagent_client.rs index 512f8d9e62b3..1b627aac9ca4 100644 --- a/crates/goose/src/agents/subagent_client.rs +++ b/crates/goose/src/agents/subagent_client.rs @@ -153,6 +153,18 @@ impl McpClientTrait for SubagentClient { ])))); } + if let Some(session_id) = &self.context.session_id { + if let Ok(session) = + crate::session::SessionManager::get_session(session_id, false).await + { + if session.session_type == crate::session::SessionType::SubAgent { + return Ok(ToolCallResult::from(Ok(CallToolResult::error(vec![ + Content::text("Subagents cannot create other subagents"), + ])))); + } + } + } + let Some(provider) = self.get_provider().await else { return Ok(ToolCallResult::from(Ok(CallToolResult::error(vec![ Content::text("No provider configured"), From d5c1af7fed3eedfd6831ad7115e80f51d09c720b Mon Sep 17 00:00:00 2001 From: Tyler Longwell Date: Wed, 17 Dec 2025 19:32:31 -0500 Subject: [PATCH 05/17] module_path vs path for read_module --- crates/goose/src/agents/code_execution_extension.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/goose/src/agents/code_execution_extension.rs b/crates/goose/src/agents/code_execution_extension.rs index 7aec94d00f95..4a9d97e601ea 100644 --- a/crates/goose/src/agents/code_execution_extension.rs +++ b/crates/goose/src/agents/code_execution_extension.rs @@ -365,9 +365,9 @@ impl CodeExecutionClient { ) -> Result, String> { let path = arguments .as_ref() - .and_then(|a| a.get("path")) + .and_then(|a| a.get("module_path")) .and_then(|v| v.as_str()) - .ok_or("Missing required parameter: path")?; + .ok_or("Missing required parameter: module_path")?; let tools = self.get_tool_infos().await; let parts: Vec<&str> = path.trim_start_matches('/').split('/').collect(); @@ -781,7 +781,10 @@ mod tests { let client = CodeExecutionClient::new(PlatformExtensionContext::default()).unwrap(); let mut args = JsonObject::new(); - args.insert("path".to_string(), Value::String("nonexistent".to_string())); + args.insert( + "module_path".to_string(), + Value::String("nonexistent".to_string()), + ); let result = client.handle_read_module(Some(args)).await; assert!(result.is_err()); From 501ffa676dcf6590fbce1fe104ed0aa4f72e19cd Mon Sep 17 00:00:00 2001 From: Tyler Longwell Date: Wed, 17 Dec 2025 21:00:35 -0500 Subject: [PATCH 06/17] correctly set subagent cwd to the parent's --- crates/goose/src/agents/subagent_client.rs | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/crates/goose/src/agents/subagent_client.rs b/crates/goose/src/agents/subagent_client.rs index 1b627aac9ca4..68bab75a6c2d 100644 --- a/crates/goose/src/agents/subagent_client.rs +++ b/crates/goose/src/agents/subagent_client.rs @@ -153,26 +153,22 @@ impl McpClientTrait for SubagentClient { ])))); } - if let Some(session_id) = &self.context.session_id { - if let Ok(session) = - crate::session::SessionManager::get_session(session_id, false).await - { - if session.session_type == crate::session::SessionType::SubAgent { - return Ok(ToolCallResult::from(Ok(CallToolResult::error(vec![ - Content::text("Subagents cannot create other subagents"), - ])))); - } - } - } - let Some(provider) = self.get_provider().await else { return Ok(ToolCallResult::from(Ok(CallToolResult::error(vec![ Content::text("No provider configured"), ])))); }; + // Get working_dir from parent session if available + let working_dir = match &self.context.session_id { + Some(session_id) => crate::session::SessionManager::get_session(session_id, false) + .await + .map(|s| s.working_dir) + .unwrap_or_else(|_| self.get_working_dir()), + None => self.get_working_dir(), + }; + let extensions = self.get_extensions().await; - let working_dir = self.get_working_dir(); let sub_recipes = self.get_sub_recipes().await; let task_config = TaskConfig::new(provider, extensions); let arguments_value = arguments From 7617ac5389e567d44dccf910ee5ff15903804508 Mon Sep 17 00:00:00 2001 From: Tyler Longwell Date: Wed, 17 Dec 2025 21:30:07 -0500 Subject: [PATCH 07/17] last expression is not the result if the call is multiline? --- crates/goose/src/agents/code_execution_extension.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/goose/src/agents/code_execution_extension.rs b/crates/goose/src/agents/code_execution_extension.rs index 4a9d97e601ea..c63f2cfbfe12 100644 --- a/crates/goose/src/agents/code_execution_extension.rs +++ b/crates/goose/src/agents/code_execution_extension.rs @@ -608,7 +608,7 @@ impl McpClientTrait for CodeExecutionClient { - Import: import { tool1, tool2 } from "serverName"; - Call: toolName({ param1: value, param2: value }) - All calls are synchronous, return strings - - Last expression is the result + - To capture output: const r = ; r - No comments in code BEFORE CALLING: Use read_module("server") to check required parameters. From cc4ecd3e8ef50e4968ccf7875fdf84fbcb331b27 Mon Sep 17 00:00:00 2001 From: Tyler Longwell Date: Wed, 17 Dec 2025 22:28:29 -0500 Subject: [PATCH 08/17] remove dead working_dir threading --- crates/goose-cli/src/session/builder.rs | 1 - crates/goose/src/agents/extension.rs | 2 -- crates/goose/src/agents/extension_manager.rs | 1 - crates/goose/src/agents/subagent_client.rs | 14 +++----------- crates/goose/src/execution/manager.rs | 1 - crates/goose/tests/agent.rs | 1 - 6 files changed, 3 insertions(+), 17 deletions(-) diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs index 4353c4e25c23..01985bdb076a 100644 --- a/crates/goose-cli/src/session/builder.rs +++ b/crates/goose-cli/src/session/builder.rs @@ -387,7 +387,6 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { session_id: Some(session_id.clone()), extension_manager: Some(Arc::downgrade(&agent.extension_manager)), tool_route_manager: Some(Arc::downgrade(&agent.tool_route_manager)), - working_dir: None, sub_recipes: Some(agent.sub_recipes()), }) .await; diff --git a/crates/goose/src/agents/extension.rs b/crates/goose/src/agents/extension.rs index 83b8a2c3ddc7..ff8204daf0a4 100644 --- a/crates/goose/src/agents/extension.rs +++ b/crates/goose/src/agents/extension.rs @@ -6,7 +6,6 @@ use crate::agents::subagent_client; use crate::agents::todo_extension; use crate::recipe::SubRecipe; use std::collections::HashMap; -use std::path::PathBuf; use std::sync::Arc; use tokio::sync::RwLock; @@ -126,7 +125,6 @@ pub struct PlatformExtensionContext { Option>, pub tool_route_manager: Option>, - pub working_dir: Option, pub sub_recipes: Option>>>, } diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index 8d27efa3a05b..6cb59d0f8a0f 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -262,7 +262,6 @@ impl ExtensionManager { session_id: None, extension_manager: None, tool_route_manager: None, - working_dir: None, sub_recipes: None, }), provider, diff --git a/crates/goose/src/agents/subagent_client.rs b/crates/goose/src/agents/subagent_client.rs index 68bab75a6c2d..301229a0bdde 100644 --- a/crates/goose/src/agents/subagent_client.rs +++ b/crates/goose/src/agents/subagent_client.rs @@ -14,7 +14,6 @@ use rmcp::model::{ ServerCapabilities, ServerNotification, Tool, ToolsCapability, }; use serde_json::Value; -use std::path::PathBuf; use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; @@ -81,13 +80,6 @@ impl SubagentClient { } } - fn get_working_dir(&self) -> PathBuf { - self.context - .working_dir - .clone() - .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))) - } - async fn build_tool(&self) -> Tool { let sub_recipes = self.get_sub_recipes().await; let sub_recipes_vec: Vec<_> = sub_recipes.values().cloned().collect(); @@ -159,13 +151,13 @@ impl McpClientTrait for SubagentClient { ])))); }; - // Get working_dir from parent session if available + // Get working_dir from parent session, fall back to current dir let working_dir = match &self.context.session_id { Some(session_id) => crate::session::SessionManager::get_session(session_id, false) .await .map(|s| s.working_dir) - .unwrap_or_else(|_| self.get_working_dir()), - None => self.get_working_dir(), + .unwrap_or_else(|_| std::env::current_dir().unwrap_or_else(|_| ".".into())), + None => std::env::current_dir().unwrap_or_else(|_| ".".into()), }; let extensions = self.get_extensions().await; diff --git a/crates/goose/src/execution/manager.rs b/crates/goose/src/execution/manager.rs index 08ea38aad2eb..f7ae8cbc5c1e 100644 --- a/crates/goose/src/execution/manager.rs +++ b/crates/goose/src/execution/manager.rs @@ -88,7 +88,6 @@ impl AgentManager { session_id: Some(session_id.clone()), extension_manager: Some(Arc::downgrade(&agent.extension_manager)), tool_route_manager: Some(Arc::downgrade(&agent.tool_route_manager)), - working_dir: None, sub_recipes: Some(agent.sub_recipes()), }) .await; diff --git a/crates/goose/tests/agent.rs b/crates/goose/tests/agent.rs index 285c1dc7d92d..bbbb0fc188ac 100644 --- a/crates/goose/tests/agent.rs +++ b/crates/goose/tests/agent.rs @@ -474,7 +474,6 @@ mod tests { session_id: Some("test_session".to_string()), extension_manager: Some(Arc::downgrade(&agent.extension_manager)), tool_route_manager: Some(Arc::downgrade(&agent.tool_route_manager)), - working_dir: None, sub_recipes: Some(agent.sub_recipes()), }) .await; From bc2adc970d90e8d92e5b6ccf98fdc00b233919e5 Mon Sep 17 00:00:00 2001 From: Tyler Longwell Date: Thu, 18 Dec 2025 12:29:16 -0500 Subject: [PATCH 09/17] remove unused _session, use ServerCapabilities builder, ToolCallResult renamed to DeferredToolCall --- crates/goose/src/agents/agent.rs | 27 ++++++------------- crates/goose/src/agents/extension_manager.rs | 4 +-- crates/goose/src/agents/final_output_tool.rs | 10 +++---- crates/goose/src/agents/mcp_client.rs | 6 ++--- crates/goose/src/agents/subagent_client.rs | 21 +++++---------- crates/goose/src/agents/subagent_tool.rs | 14 +++++----- crates/goose/src/agents/tool_execution.rs | 10 +++---- crates/goose/src/agents/tool_route_manager.rs | 6 ++--- 8 files changed, 37 insertions(+), 61 deletions(-) diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index 4e6afd594310..9d248998d9a8 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -10,7 +10,7 @@ use uuid::Uuid; use super::final_output_tool::FinalOutputTool; use super::platform_tools; -use super::tool_execution::{ToolCallResult, CHAT_MODE_TOOL_SKIPPED_RESPONSE, DECLINED_RESPONSE}; +use super::tool_execution::{DeferredToolCall, CHAT_MODE_TOOL_SKIPPED_RESPONSE, DECLINED_RESPONSE}; use crate::action_required_manager::ActionRequiredManager; use crate::agents::extension::{ExtensionConfig, ExtensionError, ExtensionResult, ToolInfo}; use crate::agents::extension_manager::{get_parameter_names, ExtensionManager}; @@ -307,7 +307,6 @@ impl Agent { permission_check_result: &PermissionCheckResult, request_to_response_map: &HashMap>>, cancel_token: Option, - session: &Session, ) -> Result> { let mut tool_futures: Vec<(String, ToolStream)> = Vec::new(); @@ -315,12 +314,7 @@ impl Agent { for request in &permission_check_result.approved { if let Ok(tool_call) = request.tool_call.clone() { let (req_id, tool_result) = self - .dispatch_tool_call( - tool_call, - request.id.clone(), - cancel_token.clone(), - session, - ) + .dispatch_tool_call(tool_call, request.id.clone(), cancel_token.clone()) .await; tool_futures.push(( @@ -435,8 +429,7 @@ impl Agent { tool_call: CallToolRequestParam, request_id: String, cancellation_token: Option, - _session: &Session, - ) -> (String, Result) { + ) -> (String, Result) { if tool_call.name == PLATFORM_MANAGE_SCHEDULE_TOOL_NAME { let arguments = tool_call .arguments @@ -451,7 +444,7 @@ impl Agent { is_error: Some(false), meta: None, }); - return (request_id, Ok(ToolCallResult::from(wrapped_result))); + return (request_id, Ok(DeferredToolCall::from(wrapped_result))); } if tool_call.name == FINAL_OUTPUT_TOOL_NAME { @@ -471,9 +464,8 @@ impl Agent { } debug!("WAITING_TOOL_START: {}", tool_call.name); - let result: ToolCallResult = if self.is_frontend_tool(&tool_call.name).await { - // For frontend tools, return an error indicating we need frontend execution - ToolCallResult::from(Err(ErrorData::new( + let result: DeferredToolCall = if self.is_frontend_tool(&tool_call.name).await { + DeferredToolCall::from(Err(ErrorData::new( ErrorCode::INTERNAL_ERROR, "Frontend tool execution required".to_string(), None, @@ -488,13 +480,12 @@ impl Agent { Err(e) => return (request_id, Err(e)), } } else { - // Clone the result to ensure no references to extension_manager are returned let result = self .extension_manager .dispatch_tool_call(tool_call.clone(), cancellation_token.unwrap_or_default()) .await; result.unwrap_or_else(|e| { - ToolCallResult::from(Err(ErrorData::new( + DeferredToolCall::from(Err(ErrorData::new( ErrorCode::INTERNAL_ERROR, e.to_string(), None, @@ -506,7 +497,7 @@ impl Agent { ( request_id, - Ok(ToolCallResult { + Ok(DeferredToolCall { notification_stream: result.notification_stream, result: Box::new( result @@ -1106,7 +1097,6 @@ impl Agent { &permission_check_result, &request_to_response_map, cancel_token.clone(), - &session, ).await?; let tool_futures_arc = Arc::new(Mutex::new(tool_futures)); @@ -1116,7 +1106,6 @@ impl Agent { tool_futures_arc.clone(), &request_to_response_map, cancel_token.clone(), - &session, &inspection_results, ); diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index 6cb59d0f8a0f..873f5017ef5f 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -30,7 +30,7 @@ use super::extension::{ ExtensionConfig, ExtensionError, ExtensionInfo, ExtensionResult, PlatformExtensionContext, ToolInfo, PLATFORM_EXTENSIONS, }; -use super::tool_execution::ToolCallResult; +use super::tool_execution::DeferredToolCall; use super::types::SharedProvider; use crate::agents::extension::{Envs, ProcessExit}; use crate::agents::extension_malware_check; @@ -1051,7 +1051,7 @@ impl ExtensionManager { &self, tool_call: CallToolRequestParam, cancellation_token: CancellationToken, - ) -> Result { + ) -> Result { // Dispatch tool call based on the prefix naming convention let (client_name, client) = self.get_client_for_tool(&tool_call.name) diff --git a/crates/goose/src/agents/final_output_tool.rs b/crates/goose/src/agents/final_output_tool.rs index 0adea4db4aa5..9839f5e91381 100644 --- a/crates/goose/src/agents/final_output_tool.rs +++ b/crates/goose/src/agents/final_output_tool.rs @@ -1,4 +1,4 @@ -use crate::agents::tool_execution::ToolCallResult; +use crate::agents::tool_execution::DeferredToolCall; use crate::recipe::Response; use indoc::formatdoc; use rmcp::model::{CallToolRequestParam, Content, ErrorCode, ErrorData, Tool, ToolAnnotations}; @@ -116,14 +116,14 @@ impl FinalOutputTool { } } - pub async fn execute_tool_call(&mut self, tool_call: CallToolRequestParam) -> ToolCallResult { + pub async fn execute_tool_call(&mut self, tool_call: CallToolRequestParam) -> DeferredToolCall { match tool_call.name.to_string().as_str() { FINAL_OUTPUT_TOOL_NAME => { let result = self.validate_json_output(&tool_call.arguments.into()).await; match result { Ok(parsed_value) => { self.final_output = Some(Self::parsed_final_output_string(parsed_value)); - ToolCallResult::from(Ok(rmcp::model::CallToolResult { + DeferredToolCall::from(Ok(rmcp::model::CallToolResult { content: vec![Content::text( "Final output successfully collected.".to_string(), )], @@ -132,14 +132,14 @@ impl FinalOutputTool { meta: None, })) } - Err(error) => ToolCallResult::from(Err(ErrorData { + Err(error) => DeferredToolCall::from(Err(ErrorData { code: ErrorCode::INVALID_PARAMS, message: Cow::from(error), data: None, })), } } - _ => ToolCallResult::from(Err(ErrorData { + _ => DeferredToolCall::from(Err(ErrorData { code: ErrorCode::INVALID_REQUEST, message: Cow::from(format!("Unknown tool: {}", tool_call.name)), data: None, diff --git a/crates/goose/src/agents/mcp_client.rs b/crates/goose/src/agents/mcp_client.rs index e4d21f2d5714..7a80cbbacaae 100644 --- a/crates/goose/src/agents/mcp_client.rs +++ b/crates/goose/src/agents/mcp_client.rs @@ -1,5 +1,5 @@ use crate::action_required_manager::ActionRequiredManager; -use crate::agents::tool_execution::ToolCallResult; +use crate::agents::tool_execution::DeferredToolCall; use crate::agents::types::SharedProvider; use crate::session_context::SESSION_ID_HEADER; use rmcp::model::{ @@ -70,9 +70,9 @@ pub trait McpClientTrait: Send + Sync { name: &str, arguments: Option, cancel_token: CancellationToken, - ) -> Result { + ) -> Result { let result = self.call_tool(name, arguments, cancel_token).await; - Ok(ToolCallResult { + Ok(DeferredToolCall { result: Box::new(futures::future::ready(result.map_err(|e| match e { ServiceError::McpError(error_data) => error_data, _ => ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None), diff --git a/crates/goose/src/agents/subagent_client.rs b/crates/goose/src/agents/subagent_client.rs index 301229a0bdde..a4e8673dadf7 100644 --- a/crates/goose/src/agents/subagent_client.rs +++ b/crates/goose/src/agents/subagent_client.rs @@ -4,14 +4,14 @@ use crate::agents::subagent_task_config::TaskConfig; use crate::agents::subagent_tool::{ create_subagent_tool, handle_subagent_tool, SUBAGENT_TOOL_NAME, }; -use crate::agents::tool_execution::ToolCallResult; +use crate::agents::tool_execution::DeferredToolCall; use crate::config::get_enabled_extensions; use anyhow::Result; use async_trait::async_trait; use rmcp::model::{ CallToolResult, Content, GetPromptResult, Implementation, InitializeResult, JsonObject, ListPromptsResult, ListResourcesResult, ListToolsResult, ProtocolVersion, ReadResourceResult, - ServerCapabilities, ServerNotification, Tool, ToolsCapability, + ServerCapabilities, ServerNotification, Tool, }; use serde_json::Value; use tokio::sync::mpsc; @@ -30,16 +30,7 @@ impl SubagentClient { context, info: InitializeResult { protocol_version: ProtocolVersion::V_2025_03_26, - capabilities: ServerCapabilities { - tools: Some(ToolsCapability { - list_changed: Some(false), - }), - resources: None, - prompts: None, - completions: None, - experimental: None, - logging: None, - }, + capabilities: ServerCapabilities::builder().enable_tools().build(), server_info: Implementation { name: EXTENSION_NAME.to_string(), title: Some("Subagent".to_string()), @@ -138,15 +129,15 @@ impl McpClientTrait for SubagentClient { name: &str, arguments: Option, cancellation_token: CancellationToken, - ) -> Result { + ) -> Result { if name != SUBAGENT_TOOL_NAME { - return Ok(ToolCallResult::from(Ok(CallToolResult::error(vec![ + return Ok(DeferredToolCall::from(Ok(CallToolResult::error(vec![ Content::text(format!("Unknown tool: {}", name)), ])))); } let Some(provider) = self.get_provider().await else { - return Ok(ToolCallResult::from(Ok(CallToolResult::error(vec![ + return Ok(DeferredToolCall::from(Ok(CallToolResult::error(vec![ Content::text("No provider configured"), ])))); }; diff --git a/crates/goose/src/agents/subagent_tool.rs b/crates/goose/src/agents/subagent_tool.rs index 9a56122e7135..8cd30cfa1a16 100644 --- a/crates/goose/src/agents/subagent_tool.rs +++ b/crates/goose/src/agents/subagent_tool.rs @@ -11,7 +11,7 @@ use tokio_util::sync::CancellationToken; use crate::agents::subagent_handler::run_complete_subagent_task; use crate::agents::subagent_task_config::TaskConfig; -use crate::agents::tool_execution::ToolCallResult; +use crate::agents::tool_execution::DeferredToolCall; use crate::providers; use crate::recipe::build_recipe::build_recipe_from_template; use crate::recipe::local_recipes::load_local_recipe_file; @@ -181,11 +181,11 @@ pub fn handle_subagent_tool( sub_recipes: HashMap, working_dir: PathBuf, cancellation_token: Option, -) -> ToolCallResult { +) -> DeferredToolCall { let parsed_params: SubagentParams = match serde_json::from_value(params) { Ok(p) => p, Err(e) => { - return ToolCallResult::from(Err(ErrorData { + return DeferredToolCall::from(Err(ErrorData { code: ErrorCode::INVALID_PARAMS, message: Cow::from(format!("Invalid parameters: {}", e)), data: None, @@ -194,7 +194,7 @@ pub fn handle_subagent_tool( }; if parsed_params.instructions.is_none() && parsed_params.subrecipe.is_none() { - return ToolCallResult::from(Err(ErrorData { + return DeferredToolCall::from(Err(ErrorData { code: ErrorCode::INVALID_PARAMS, message: Cow::from("Must provide 'instructions' or 'subrecipe' (or both)"), data: None, @@ -202,7 +202,7 @@ pub fn handle_subagent_tool( } if parsed_params.parameters.is_some() && parsed_params.subrecipe.is_none() { - return ToolCallResult::from(Err(ErrorData { + return DeferredToolCall::from(Err(ErrorData { code: ErrorCode::INVALID_PARAMS, message: Cow::from("'parameters' can only be used with 'subrecipe'"), data: None, @@ -212,7 +212,7 @@ pub fn handle_subagent_tool( let recipe = match build_recipe(&parsed_params, &sub_recipes) { Ok(r) => r, Err(e) => { - return ToolCallResult::from(Err(ErrorData { + return DeferredToolCall::from(Err(ErrorData { code: ErrorCode::INVALID_PARAMS, message: Cow::from(e.to_string()), data: None, @@ -220,7 +220,7 @@ pub fn handle_subagent_tool( } }; - ToolCallResult { + DeferredToolCall { notification_stream: None, result: Box::new( execute_subagent( diff --git a/crates/goose/src/agents/tool_execution.rs b/crates/goose/src/agents/tool_execution.rs index 5323561cd3f0..ff41bb07c9d7 100644 --- a/crates/goose/src/agents/tool_execution.rs +++ b/crates/goose/src/agents/tool_execution.rs @@ -13,14 +13,12 @@ use crate::mcp_utils::ToolResult; use crate::permission::Permission; use rmcp::model::{Content, ServerNotification}; -// ToolCallResult combines the result of a tool call with an optional notification stream that -// can be used to receive notifications from the tool. -pub struct ToolCallResult { +pub struct DeferredToolCall { pub result: Box> + Send + Unpin>, pub notification_stream: Option + Send + Unpin>>, } -impl From> for ToolCallResult { +impl From> for DeferredToolCall { fn from(result: ToolResult) -> Self { Self { result: Box::new(futures::future::ready(result)), @@ -32,7 +30,6 @@ impl From> for ToolCallResult { use super::agent::{tool_stream, ToolStream}; use crate::agents::Agent; use crate::conversation::message::{Message, ToolRequest}; -use crate::session::Session; use crate::tool_inspection::get_security_finding_id_from_results; pub const DECLINED_RESPONSE: &str = "The user has declined to run this tool. \ @@ -55,7 +52,6 @@ impl Agent { tool_futures: Arc>>, request_to_response_map: &'a HashMap>>, cancellation_token: Option, - session: &'a Session, inspection_results: &'a [crate::tool_inspection::InspectionResult], ) -> BoxStream<'a, anyhow::Result> { try_stream! { @@ -96,7 +92,7 @@ impl Agent { } if confirmation.permission == Permission::AllowOnce || confirmation.permission == Permission::AlwaysAllow { - let (req_id, tool_result) = self.dispatch_tool_call(tool_call.clone(), request.id.clone(), cancellation_token.clone(), session).await; + let (req_id, tool_result) = self.dispatch_tool_call(tool_call.clone(), request.id.clone(), cancellation_token.clone()).await; let mut futures = tool_futures.lock().await; futures.push((req_id, match tool_result { diff --git a/crates/goose/src/agents/tool_route_manager.rs b/crates/goose/src/agents/tool_route_manager.rs index c757a173aefc..9fd91cb119fc 100644 --- a/crates/goose/src/agents/tool_route_manager.rs +++ b/crates/goose/src/agents/tool_route_manager.rs @@ -1,7 +1,7 @@ use crate::agents::extension_manager::ExtensionManager; use crate::agents::router_tool_selector::{create_tool_selector, RouterToolSelector}; use crate::agents::router_tools::{self}; -use crate::agents::tool_execution::ToolCallResult; +use crate::agents::tool_execution::DeferredToolCall; use crate::agents::tool_router_index_manager::ToolRouterIndexManager; use crate::config::Config; use crate::conversation::message::ToolRequest; @@ -52,11 +52,11 @@ impl ToolRouteManager { pub async fn dispatch_route_search_tool( &self, arguments: JsonObject, - ) -> Result { + ) -> Result { let selector = self.router_tool_selector.lock().await.clone(); match selector.as_ref() { Some(selector) => match selector.select_tools(arguments).await { - Ok(content) => Ok(ToolCallResult::from(Ok(rmcp::model::CallToolResult { + Ok(content) => Ok(DeferredToolCall::from(Ok(rmcp::model::CallToolResult { content, structured_content: None, is_error: Some(false), From b08a336335f080bfdd2128a9730b5ffdba687871 Mon Sep 17 00:00:00 2001 From: Tyler Longwell Date: Thu, 18 Dec 2025 13:49:28 -0500 Subject: [PATCH 10/17] respect user config for `subagent` extension --- crates/goose/src/agents/agent.rs | 4 ---- crates/goose/src/agents/extension.rs | 2 +- crates/goose/src/agents/subagent_client.rs | 22 ++++++++++++++++++---- crates/goose/src/config/extensions.rs | 2 +- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index 9d248998d9a8..58f075cd6775 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -264,10 +264,6 @@ impl Agent { let initial_messages = conversation.messages().clone(); let config = Config::global(); - if let Err(e) = self.enable_subagent_extension().await { - warn!("Failed to enable subagent extension: {}", e); - } - let (tools, toolshim_tools, system_prompt) = self.prepare_tools_and_prompt(working_dir).await?; let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto); diff --git a/crates/goose/src/agents/extension.rs b/crates/goose/src/agents/extension.rs index ff8204daf0a4..d99eeac90f1b 100644 --- a/crates/goose/src/agents/extension.rs +++ b/crates/goose/src/agents/extension.rs @@ -109,7 +109,7 @@ pub static PLATFORM_EXTENSIONS: Lazy PlatformExtensionDef { name: subagent_client::EXTENSION_NAME, description: "Delegate tasks to independent subagents", - default_enabled: false, + default_enabled: true, client_factory: |ctx| Box::new(subagent_client::SubagentClient::new(ctx).unwrap()), }, ); diff --git a/crates/goose/src/agents/subagent_client.rs b/crates/goose/src/agents/subagent_client.rs index a4e8673dadf7..404d00c4e79a 100644 --- a/crates/goose/src/agents/subagent_client.rs +++ b/crates/goose/src/agents/subagent_client.rs @@ -6,6 +6,7 @@ use crate::agents::subagent_tool::{ }; use crate::agents::tool_execution::DeferredToolCall; use crate::config::get_enabled_extensions; +use crate::session::{SessionManager, SessionType}; use anyhow::Result; use async_trait::async_trait; use rmcp::model::{ @@ -52,7 +53,7 @@ impl SubagentClient { } async fn get_extensions(&self) -> Vec { - if let Some(em) = self + let extensions = if let Some(em) = self .context .extension_manager .as_ref() @@ -61,7 +62,11 @@ impl SubagentClient { em.get_extension_configs().await } else { get_enabled_extensions() - } + }; + extensions + .into_iter() + .filter(|ext| ext.name() != EXTENSION_NAME) + .collect() } async fn get_sub_recipes(&self) -> std::collections::HashMap { @@ -136,15 +141,24 @@ impl McpClientTrait for SubagentClient { ])))); } + if let Some(ref session_id) = self.context.session_id { + if let Ok(session) = SessionManager::get_session(session_id, false).await { + if session.session_type == SessionType::SubAgent { + return Ok(DeferredToolCall::from(Ok(CallToolResult::error(vec![ + Content::text("Subagents cannot spawn subagents."), + ])))); + } + } + } + let Some(provider) = self.get_provider().await else { return Ok(DeferredToolCall::from(Ok(CallToolResult::error(vec![ Content::text("No provider configured"), ])))); }; - // Get working_dir from parent session, fall back to current dir let working_dir = match &self.context.session_id { - Some(session_id) => crate::session::SessionManager::get_session(session_id, false) + Some(session_id) => SessionManager::get_session(session_id, false) .await .map(|s| s.working_dir) .unwrap_or_else(|_| std::env::current_dir().unwrap_or_else(|_| ".".into())), diff --git a/crates/goose/src/config/extensions.rs b/crates/goose/src/config/extensions.rs index 4ad4e84de300..a7bf586c8dec 100644 --- a/crates/goose/src/config/extensions.rs +++ b/crates/goose/src/config/extensions.rs @@ -66,7 +66,7 @@ fn get_extensions_map() -> IndexMap { bundled: Some(true), available_tools: Vec::new(), }, - enabled: true, + enabled: def.default_enabled, }, ); } From eef0d0fe06c7294934252dd28c74083d947ceea5 Mon Sep 17 00:00:00 2001 From: Tyler Longwell Date: Thu, 18 Dec 2025 14:52:51 -0500 Subject: [PATCH 11/17] need to enable the subagent tool whenever subrecipes are specified --- crates/goose-cli/src/session/builder.rs | 2 ++ crates/goose-server/src/routes/recipe_utils.rs | 2 ++ crates/goose/src/agents/agent.rs | 13 +++++++++++-- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs index 01985bdb076a..62a9a524ce69 100644 --- a/crates/goose-cli/src/session/builder.rs +++ b/crates/goose-cli/src/session/builder.rs @@ -497,6 +497,8 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { } } + agent_ptr.ensure_subagent_for_recipes().await; + // Determine editor mode let edit_mode = config .get_param::("EDIT_MODE") diff --git a/crates/goose-server/src/routes/recipe_utils.rs b/crates/goose-server/src/routes/recipe_utils.rs index c1c7f45ca61f..a352e4cb43d7 100644 --- a/crates/goose-server/src/routes/recipe_utils.rs +++ b/crates/goose-server/src/routes/recipe_utils.rs @@ -170,6 +170,8 @@ pub async fn apply_recipe_to_agent( ) .await; + agent.ensure_subagent_for_recipes().await; + recipe.instructions.as_ref().map(|instructions| { let mut context: HashMap<&str, Value> = HashMap::new(); context.insert("recipe_instructions", Value::String(instructions.clone())); diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index 58f075cd6775..77b86345f9ae 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -407,8 +407,8 @@ impl Agent { response: Option, include_final_output: bool, ) { - if let Some(sub_recipes) = sub_recipes { - self.add_sub_recipes(sub_recipes).await; + if let Some(ref sub_recipes) = sub_recipes { + self.add_sub_recipes(sub_recipes.clone()).await; } if include_final_output { @@ -418,6 +418,15 @@ impl Agent { } } + pub async fn ensure_subagent_for_recipes(&self) { + let has_sub_recipes = !self.sub_recipes.read().await.is_empty(); + if has_sub_recipes { + if let Err(e) = self.enable_subagent_extension().await { + warn!("Failed to enable subagent extension for recipe: {}", e); + } + } + } + /// Dispatch a single tool call to the appropriate client #[instrument(skip(self, tool_call, request_id), fields(input, output))] pub async fn dispatch_tool_call( From 73423df79c0b5fd751acc465b7948783fbc74f8b Mon Sep 17 00:00:00 2001 From: Tyler Longwell Date: Sun, 25 Jan 2026 10:14:36 -0500 Subject: [PATCH 12/17] cargo fmt --- crates/goose-cli/src/session/builder.rs | 1 - crates/goose/src/agents/final_output_tool.rs | 5 ++++- crates/goose/src/agents/mcp_client.rs | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs index 83c77f64ef6a..ae8f85ea573c 100644 --- a/crates/goose-cli/src/session/builder.rs +++ b/crates/goose-cli/src/session/builder.rs @@ -497,7 +497,6 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { process::exit(1); }); - if session_config.resume { let session = agent .config diff --git a/crates/goose/src/agents/final_output_tool.rs b/crates/goose/src/agents/final_output_tool.rs index 3bba80d089ca..a92fc9d4550a 100644 --- a/crates/goose/src/agents/final_output_tool.rs +++ b/crates/goose/src/agents/final_output_tool.rs @@ -116,7 +116,10 @@ impl FinalOutputTool { } } - pub async fn execute_tool_call(&mut self, tool_call: CallToolRequestParams) -> DeferredToolCall { + pub async fn execute_tool_call( + &mut self, + tool_call: CallToolRequestParams, + ) -> DeferredToolCall { match tool_call.name.to_string().as_str() { FINAL_OUTPUT_TOOL_NAME => { let result = self.validate_json_output(&tool_call.arguments.into()).await; diff --git a/crates/goose/src/agents/mcp_client.rs b/crates/goose/src/agents/mcp_client.rs index f6e83d4c6795..921135e79d3b 100644 --- a/crates/goose/src/agents/mcp_client.rs +++ b/crates/goose/src/agents/mcp_client.rs @@ -67,7 +67,9 @@ pub trait McpClientTrait: Send + Sync { arguments: Option, cancel_token: CancellationToken, ) -> Result { - let result = self.call_tool(session_id, name, arguments, cancel_token).await; + let result = self + .call_tool(session_id, name, arguments, cancel_token) + .await; Ok(DeferredToolCall { result: Box::new(futures::future::ready(result.map_err(|e| match e { ServiceError::McpError(error_data) => error_data, From cac97731432973c1bb533b55cfcffcc915e11838 Mon Sep 17 00:00:00 2001 From: Tyler Longwell Date: Sun, 25 Jan 2026 11:32:38 -0500 Subject: [PATCH 13/17] remove enable_subagent_extension, fix mcp integration test, enable call_tool for subagent --- clippy-baselines/too_many_lines.txt | 1 + crates/goose/src/agents/agent.rs | 33 +++++--------------- crates/goose/src/agents/extension_manager.rs | 7 +++-- crates/goose/src/agents/subagent_client.rs | 19 +++++------ crates/goose/tests/mcp_integration_test.rs | 2 +- 5 files changed, 21 insertions(+), 41 deletions(-) diff --git a/clippy-baselines/too_many_lines.txt b/clippy-baselines/too_many_lines.txt index ae7ca874e58c..47ada3c2b352 100644 --- a/clippy-baselines/too_many_lines.txt +++ b/clippy-baselines/too_many_lines.txt @@ -23,3 +23,4 @@ crates/goose/src/providers/formats/google.rs::format_messages crates/goose/src/providers/formats/openai.rs::format_messages crates/goose/src/providers/formats/openai.rs::response_to_streaming_message crates/goose/src/providers/snowflake.rs::post +crates/goose-bench/src/eval_suites/core/developer/simple_repo_clone_test.rs::run diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index 7a6f99e5934f..84372d1e1409 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -194,11 +194,16 @@ impl Agent { let session_manager = Arc::clone(&config.session_manager); let permission_manager = Arc::clone(&config.permission_manager); + let sub_recipes = Arc::new(tokio::sync::RwLock::new(HashMap::new())); Self { provider: provider.clone(), config, - extension_manager: Arc::new(ExtensionManager::new(provider.clone(), session_manager)), - sub_recipes: Arc::new(tokio::sync::RwLock::new(HashMap::new())), + extension_manager: Arc::new(ExtensionManager::new( + provider.clone(), + session_manager, + Some(sub_recipes.clone()), + )), + sub_recipes, final_output_tool: Arc::new(Mutex::new(None)), frontend_tools: Mutex::new(HashMap::new()), frontend_instructions: Mutex::new(None), @@ -786,30 +791,6 @@ impl Agent { prefixed_tools } - pub async fn enable_subagent_extension(&self, session_id: &str) -> ExtensionResult<()> { - if !self.subagents_enabled(session_id).await { - return Ok(()); - } - if self - .extension_manager - .is_extension_enabled(subagent_client::EXTENSION_NAME) - .await - { - return Ok(()); - } - self.extension_manager - .add_extension_with_working_dir( - ExtensionConfig::Platform { - name: subagent_client::EXTENSION_NAME.to_string(), - description: "Delegate tasks to independent subagents".to_string(), - bundled: Some(true), - available_tools: vec![], - }, - None, - ) - .await - } - pub async fn remove_extension(&self, name: &str) -> Result<()> { self.extension_manager.remove_extension(name).await?; Ok(()) diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index 3bcd03048027..2da060bd9161 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -443,13 +443,16 @@ impl ExtensionManager { pub fn new( provider: SharedProvider, session_manager: Arc, + sub_recipes: Option< + Arc>>, + >, ) -> Self { Self { extensions: Mutex::new(HashMap::new()), context: PlatformExtensionContext { extension_manager: None, session_manager, - sub_recipes: None, + sub_recipes, }, provider, tools_cache: Mutex::new(None), @@ -460,7 +463,7 @@ impl ExtensionManager { #[cfg(test)] pub fn new_without_provider(data_dir: std::path::PathBuf) -> Self { let session_manager = Arc::new(crate::session::SessionManager::new(data_dir)); - Self::new(Arc::new(Mutex::new(None)), session_manager) + Self::new(Arc::new(Mutex::new(None)), session_manager, None) } pub fn get_context(&self) -> &PlatformExtensionContext { diff --git a/crates/goose/src/agents/subagent_client.rs b/crates/goose/src/agents/subagent_client.rs index b059a82e427c..f62d206b99bc 100644 --- a/crates/goose/src/agents/subagent_client.rs +++ b/crates/goose/src/agents/subagent_client.rs @@ -122,20 +122,15 @@ impl McpClientTrait for SubagentClient { async fn call_tool( &self, - _session_id: &str, + session_id: &str, name: &str, - _arguments: Option, - _cancellation_token: CancellationToken, + arguments: Option, + cancellation_token: CancellationToken, ) -> Result { - if name != SUBAGENT_TOOL_NAME { - return Ok(CallToolResult::error(vec![Content::text(format!( - "Unknown tool: {}", - name - ))])); - } - Ok(CallToolResult::error(vec![Content::text( - "Subagent tool must be called via call_tool_deferred", - )])) + let deferred = self + .call_tool_deferred(session_id, name, arguments, cancellation_token) + .await?; + deferred.result.await.map_err(Error::McpError) } async fn call_tool_deferred( diff --git a/crates/goose/tests/mcp_integration_test.rs b/crates/goose/tests/mcp_integration_test.rs index 28d43ece795e..55c14469e2c2 100644 --- a/crates/goose/tests/mcp_integration_test.rs +++ b/crates/goose/tests/mcp_integration_test.rs @@ -243,7 +243,7 @@ async fn test_replayed_session( let session_manager = Arc::new(goose::session::SessionManager::new( temp_dir.path().to_path_buf(), )); - let extension_manager = Arc::new(ExtensionManager::new(provider, session_manager)); + let extension_manager = Arc::new(ExtensionManager::new(provider, session_manager, None)); #[allow(clippy::redundant_closure_call)] let result = (async || -> Result<(), Box> { From 9bba173f1212dfdf0954832b2a971fd02c2f2511 Mon Sep 17 00:00:00 2001 From: Tyler Longwell Date: Sun, 25 Jan 2026 13:37:01 -0500 Subject: [PATCH 14/17] defense in depth --- crates/goose-cli/src/session/builder.rs | 2 +- crates/goose-server/src/routes/agent.rs | 7 ++- .../goose-server/src/routes/recipe_utils.rs | 3 +- crates/goose-server/src/routes/session.rs | 2 +- crates/goose/src/agents/agent.rs | 60 ++++++++++--------- .../src/agents/code_execution_extension.rs | 1 + crates/goose/src/agents/extension.rs | 3 +- crates/goose/src/agents/extension_manager.rs | 9 ++- crates/goose/src/agents/subagent_client.rs | 12 ++++ crates/goose/tests/mcp_integration_test.rs | 7 ++- 10 files changed, 71 insertions(+), 35 deletions(-) diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs index ae8f85ea573c..3fac45eff77c 100644 --- a/crates/goose-cli/src/session/builder.rs +++ b/crates/goose-cli/src/session/builder.rs @@ -587,7 +587,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { ) .await; - agent_ptr.ensure_subagent_for_recipes().await; + agent_ptr.ensure_subagent_extension(&session_id).await; // Determine editor mode let edit_mode = config diff --git a/crates/goose-server/src/routes/agent.rs b/crates/goose-server/src/routes/agent.rs index 5e54a7b6d12e..789fa9ea17d4 100644 --- a/crates/goose-server/src/routes/agent.rs +++ b/crates/goose-server/src/routes/agent.rs @@ -426,7 +426,9 @@ async fn update_from_session( .await { Ok(Some(recipe)) => { - if let Some(prompt) = apply_recipe_to_agent(&agent, &recipe, true).await { + if let Some(prompt) = + apply_recipe_to_agent(&agent, &recipe, &payload.session_id, true).await + { update_prompt = prompt; } } @@ -700,7 +702,8 @@ async fn restart_agent_internal( .await { Ok(Some(recipe)) => { - if let Some(prompt) = apply_recipe_to_agent(&agent, &recipe, true).await { + if let Some(prompt) = apply_recipe_to_agent(&agent, &recipe, session_id, true).await + { update_prompt = prompt; } } diff --git a/crates/goose-server/src/routes/recipe_utils.rs b/crates/goose-server/src/routes/recipe_utils.rs index 8632e5bbe3f0..68cf978cef1e 100644 --- a/crates/goose-server/src/routes/recipe_utils.rs +++ b/crates/goose-server/src/routes/recipe_utils.rs @@ -160,6 +160,7 @@ pub async fn build_recipe_with_parameter_values( pub async fn apply_recipe_to_agent( agent: &Arc, recipe: &Recipe, + session_id: &str, include_final_output_tool: bool, ) -> Option { agent @@ -170,7 +171,7 @@ pub async fn apply_recipe_to_agent( ) .await; - agent.ensure_subagent_for_recipes().await; + agent.ensure_subagent_extension(session_id).await; recipe.instructions.as_ref().map(|instructions| { let mut context: HashMap<&str, Value> = HashMap::new(); diff --git a/crates/goose-server/src/routes/session.rs b/crates/goose-server/src/routes/session.rs index 148c132bf730..4e5f26651cdd 100644 --- a/crates/goose-server/src/routes/session.rs +++ b/crates/goose-server/src/routes/session.rs @@ -246,7 +246,7 @@ async fn update_session_user_recipe_values( message: format!("Failed to get agent: {}", status), status, })?; - if let Some(prompt) = apply_recipe_to_agent(&agent, &recipe, false).await { + if let Some(prompt) = apply_recipe_to_agent(&agent, &recipe, &session_id, false).await { agent.extend_system_prompt(prompt).await; } Ok(Json(UpdateSessionUserRecipeValuesResponse { recipe })) diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index 84372d1e1409..97919a758419 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -22,7 +22,7 @@ use crate::agents::retry::{RetryManager, RetryResult}; use crate::agents::subagent_client; use crate::agents::types::{FrontendTool, SessionConfig, SharedProvider, ToolResultReceiver}; use crate::config::permission::PermissionManager; -use crate::config::{get_enabled_extensions, Config, GooseMode}; +use crate::config::{get_enabled_extensions, is_extension_enabled, Config, GooseMode}; use crate::context_mgmt::{ check_if_compaction_needed, compact_messages, DEFAULT_COMPACTION_THRESHOLD, }; @@ -194,6 +194,7 @@ impl Agent { let session_manager = Arc::clone(&config.session_manager); let permission_manager = Arc::clone(&config.permission_manager); + let goose_mode = config.goose_mode; let sub_recipes = Arc::new(tokio::sync::RwLock::new(HashMap::new())); Self { provider: provider.clone(), @@ -202,6 +203,7 @@ impl Agent { provider.clone(), session_manager, Some(sub_recipes.clone()), + goose_mode, )), sub_recipes, final_output_tool: Arc::new(Mutex::new(None)), @@ -457,32 +459,33 @@ impl Agent { } } - pub async fn ensure_subagent_for_recipes(&self) { - let has_sub_recipes = !self.sub_recipes.read().await.is_empty(); - if has_sub_recipes { - // Enable subagent extension without session check since we know recipes need it - if self - .extension_manager - .is_extension_enabled(subagent_client::EXTENSION_NAME) - .await - { - return; - } - if let Err(e) = self - .extension_manager - .add_extension_with_working_dir( - ExtensionConfig::Platform { - name: subagent_client::EXTENSION_NAME.to_string(), - description: "Delegate tasks to independent subagents".to_string(), - bundled: Some(true), - available_tools: vec![], - }, - None, - ) - .await - { - warn!("Failed to enable subagent extension for recipe: {}", e); - } + pub async fn ensure_subagent_extension(&self, session_id: &str) { + if !self.subagents_enabled(session_id).await { + return; + } + + if self + .extension_manager + .is_extension_enabled(subagent_client::EXTENSION_NAME) + .await + { + return; + } + + if let Err(e) = self + .extension_manager + .add_extension_with_working_dir( + ExtensionConfig::Platform { + name: subagent_client::EXTENSION_NAME.to_string(), + description: "Delegate tasks to independent subagents".to_string(), + bundled: Some(true), + available_tools: vec![], + }, + None, + ) + .await + { + warn!("Failed to enable subagent extension: {}", e); } } @@ -738,6 +741,9 @@ impl Agent { } pub async fn subagents_enabled(&self, session_id: &str) -> bool { + if !is_extension_enabled(subagent_client::EXTENSION_NAME) { + return false; + } if self.config.goose_mode != GooseMode::Auto { return false; } diff --git a/crates/goose/src/agents/code_execution_extension.rs b/crates/goose/src/agents/code_execution_extension.rs index 9f852115151e..0c92a76bdf51 100644 --- a/crates/goose/src/agents/code_execution_extension.rs +++ b/crates/goose/src/agents/code_execution_extension.rs @@ -907,6 +907,7 @@ mod tests { extension_manager: None, session_manager, sub_recipes: None, + goose_mode: crate::config::GooseMode::Auto, } } diff --git a/crates/goose/src/agents/extension.rs b/crates/goose/src/agents/extension.rs index 0d2ffd9fcc7a..57c985eb3ac2 100644 --- a/crates/goose/src/agents/extension.rs +++ b/crates/goose/src/agents/extension.rs @@ -121,7 +121,7 @@ pub static PLATFORM_EXTENSIONS: Lazy PlatformExtensionDef { name: subagent_client::EXTENSION_NAME, description: "Delegate tasks to independent subagents", - default_enabled: true, + default_enabled: false, client_factory: |ctx| Box::new(subagent_client::SubagentClient::new(ctx).unwrap()), }, ); @@ -136,6 +136,7 @@ pub struct PlatformExtensionContext { Option>, pub session_manager: std::sync::Arc, pub sub_recipes: Option>>>, + pub goose_mode: crate::config::GooseMode, } impl PlatformExtensionContext { diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index 2da060bd9161..458997001b38 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -446,6 +446,7 @@ impl ExtensionManager { sub_recipes: Option< Arc>>, >, + goose_mode: crate::config::GooseMode, ) -> Self { Self { extensions: Mutex::new(HashMap::new()), @@ -453,6 +454,7 @@ impl ExtensionManager { extension_manager: None, session_manager, sub_recipes, + goose_mode, }, provider, tools_cache: Mutex::new(None), @@ -463,7 +465,12 @@ impl ExtensionManager { #[cfg(test)] pub fn new_without_provider(data_dir: std::path::PathBuf) -> Self { let session_manager = Arc::new(crate::session::SessionManager::new(data_dir)); - Self::new(Arc::new(Mutex::new(None)), session_manager, None) + Self::new( + Arc::new(Mutex::new(None)), + session_manager, + None, + crate::config::GooseMode::Auto, + ) } pub fn get_context(&self) -> &PlatformExtensionContext { diff --git a/crates/goose/src/agents/subagent_client.rs b/crates/goose/src/agents/subagent_client.rs index f62d206b99bc..3f01eb97b8fa 100644 --- a/crates/goose/src/agents/subagent_client.rs +++ b/crates/goose/src/agents/subagent_client.rs @@ -146,6 +146,12 @@ impl McpClientTrait for SubagentClient { ])))); } + if self.context.goose_mode != GooseMode::Auto { + return Ok(DeferredToolCall::from(Ok(CallToolResult::error(vec![ + Content::text("Subagents are only available in Auto mode."), + ])))); + } + // Check if this is already a subagent session let session_manager = Arc::clone(&self.context.session_manager); if let Ok(session) = session_manager.get_session(session_id, false).await { @@ -162,6 +168,12 @@ impl McpClientTrait for SubagentClient { ])))); }; + if provider.get_active_model_name().starts_with("gemini") { + return Ok(DeferredToolCall::from(Ok(CallToolResult::error(vec![ + Content::text("Subagents are not supported with Gemini models."), + ])))); + } + let working_dir = session_manager .get_session(session_id, false) .await diff --git a/crates/goose/tests/mcp_integration_test.rs b/crates/goose/tests/mcp_integration_test.rs index 55c14469e2c2..177778e9151e 100644 --- a/crates/goose/tests/mcp_integration_test.rs +++ b/crates/goose/tests/mcp_integration_test.rs @@ -243,7 +243,12 @@ async fn test_replayed_session( let session_manager = Arc::new(goose::session::SessionManager::new( temp_dir.path().to_path_buf(), )); - let extension_manager = Arc::new(ExtensionManager::new(provider, session_manager, None)); + let extension_manager = Arc::new(ExtensionManager::new( + provider, + session_manager, + None, + goose::config::GooseMode::Auto, + )); #[allow(clippy::redundant_closure_call)] let result = (async || -> Result<(), Box> { From 9f5d3eea04d9248f4847fd710789b5f4c53d7a73 Mon Sep 17 00:00:00 2001 From: Tyler Longwell Date: Sun, 25 Jan 2026 14:16:21 -0500 Subject: [PATCH 15/17] subagents enabled by default --- crates/goose/src/agents/extension.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/goose/src/agents/extension.rs b/crates/goose/src/agents/extension.rs index 57c985eb3ac2..497620aafc97 100644 --- a/crates/goose/src/agents/extension.rs +++ b/crates/goose/src/agents/extension.rs @@ -121,7 +121,7 @@ pub static PLATFORM_EXTENSIONS: Lazy PlatformExtensionDef { name: subagent_client::EXTENSION_NAME, description: "Delegate tasks to independent subagents", - default_enabled: false, + default_enabled: true, client_factory: |ctx| Box::new(subagent_client::SubagentClient::new(ctx).unwrap()), }, ); From ead2b0f6d974a92a79956a67280b59a3130f11b0 Mon Sep 17 00:00:00 2001 From: Tyler Longwell Date: Sun, 25 Jan 2026 15:24:08 -0500 Subject: [PATCH 16/17] rename subagent tool to prevent code mode confusion loop --- crates/goose-cli/src/session/output.rs | 2 +- crates/goose/src/agents/subagent_tool.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/goose-cli/src/session/output.rs b/crates/goose-cli/src/session/output.rs index 89b26c66c518..2d844ce6b18a 100644 --- a/crates/goose-cli/src/session/output.rs +++ b/crates/goose-cli/src/session/output.rs @@ -285,7 +285,7 @@ fn render_tool_request(req: &ToolRequest, theme: Theme, debug: bool) { "developer__text_editor" => render_text_editor_request(call, debug), "developer__shell" => render_shell_request(call, debug), "code_execution__execute_code" => render_execute_code_request(call, debug), - "subagent" => render_subagent_request(call, debug), + "subagent__delegate" => render_subagent_request(call, debug), "todo__write" => render_todo_request(call, debug), _ => render_default_request(call, debug), }, diff --git a/crates/goose/src/agents/subagent_tool.rs b/crates/goose/src/agents/subagent_tool.rs index 5e631e2053bc..2991ab099626 100644 --- a/crates/goose/src/agents/subagent_tool.rs +++ b/crates/goose/src/agents/subagent_tool.rs @@ -18,7 +18,7 @@ use crate::recipe::build_recipe::build_recipe_from_template; use crate::recipe::local_recipes::load_local_recipe_file; use crate::recipe::{Recipe, SubRecipe}; -pub const SUBAGENT_TOOL_NAME: &str = "subagent"; +pub const SUBAGENT_TOOL_NAME: &str = "delegate"; const SUMMARY_INSTRUCTIONS: &str = r#" Important: Your parent agent will only receive your final message as a summary of your work. @@ -434,13 +434,13 @@ mod tests { #[test] fn test_tool_name() { - assert_eq!(SUBAGENT_TOOL_NAME, "subagent"); + assert_eq!(SUBAGENT_TOOL_NAME, "delegate"); } #[test] fn test_create_tool_without_subrecipes() { let tool = create_subagent_tool(&[]); - assert_eq!(tool.name, "subagent"); + assert_eq!(tool.name, "delegate"); assert!(tool.description.as_ref().unwrap().contains("Ad-hoc")); assert!(!tool .description From 95d87826807cacf5c190cca0ae20e2c37f15449a Mon Sep 17 00:00:00 2001 From: Tyler Longwell Date: Sun, 25 Jan 2026 15:42:07 -0500 Subject: [PATCH 17/17] fix ci for new tool name --- scripts/test_subrecipes.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/test_subrecipes.sh b/scripts/test_subrecipes.sh index 25d5e11190c4..18d78ea5534b 100755 --- a/scripts/test_subrecipes.sh +++ b/scripts/test_subrecipes.sh @@ -77,13 +77,13 @@ check_recipe_output() { local tmpfile=$1 local mode=$2 - # Check for unified subagent tool invocation (new format: "─── subagent |") - if grep -q "─── subagent" "$tmpfile"; then - echo "✓ SUCCESS: Subagent tool invoked" - RESULTS+=("✓ Subagent tool invocation ($mode)") + # Check for delegate tool invocation (format: "─── delegate | subagent") + if grep -q "─── delegate" "$tmpfile"; then + echo "✓ SUCCESS: Delegate tool invoked" + RESULTS+=("✓ Delegate tool invocation ($mode)") else - echo "✗ FAILED: No evidence of subagent tool invocation" - RESULTS+=("✗ Subagent tool invocation ($mode)") + echo "✗ FAILED: No evidence of delegate tool invocation" + RESULTS+=("✗ Delegate tool invocation ($mode)") fi # Check that both subrecipes were called (shown as "subrecipe: " in output)