From 88a7150f7a870e5d9eb808ffa395b7ce63328dfe Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Tue, 13 Jan 2026 22:20:04 +1100 Subject: [PATCH 01/12] resolved all the entensions to load in cli --- crates/goose-cli/src/session/builder.rs | 251 ++++++++++++------------ crates/goose-cli/src/session/mod.rs | 121 +++++++----- 2 files changed, 202 insertions(+), 170 deletions(-) diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs index 75f6651a4f1d..659296013813 100644 --- a/crates/goose-cli/src/session/builder.rs +++ b/crates/goose-cli/src/session/builder.rs @@ -1,7 +1,8 @@ use super::output; use super::CliSession; use console::style; -use goose::agents::types::{RetryConfig, SessionConfig}; +use goose::agents::extension::PlatformExtensionContext; +use goose::agents::types::RetryConfig; use goose::agents::Agent; use goose::config::{ extensions::get_extension_by_name, get_all_extensions, get_enabled_extensions, Config, @@ -9,17 +10,72 @@ use goose::config::{ }; use goose::providers::create; use goose::recipe::{Response, SubRecipe}; - -use goose::agents::extension::PlatformExtensionContext; use goose::session::session_manager::SessionType; use goose::session::SessionManager; use goose::session::{EnabledExtensionsState, ExtensionState}; use rustyline::EditMode; -use std::collections::HashSet; +use std::collections::BTreeSet; use std::process; use std::sync::Arc; use tokio::task::JoinSet; +/// Maximum length for extension hint display in spinner messages +const EXTENSION_HINT_MAX_LEN: usize = 5; + +/// Truncates a string to a maximum length, appending an ellipsis if truncated. +fn truncate_with_ellipsis(s: &str, max_len: usize) -> String { + let truncated: String = s.chars().take(max_len).collect(); + if s.chars().count() > max_len { + format!("{}…", truncated) + } else { + truncated + } +} + +fn parse_cli_flag_extensions( + extensions: &[String], + streamable_http_extensions: &[String], + builtins: &[String], +) -> Vec<(String, ExtensionConfig)> { + let mut extensions_to_load = Vec::new(); + + for (idx, ext_str) in extensions.iter().enumerate() { + match CliSession::parse_stdio_extension(ext_str) { + Ok(config) => { + let hint = truncate_with_ellipsis(ext_str, EXTENSION_HINT_MAX_LEN); + let label = format!("stdio #{}({})", idx + 1, hint); + extensions_to_load.push((label, config)); + } + Err(e) => { + eprintln!( + "{}", + style(format!( + "Warning: Invalid --extension value '{}' ({}); ignoring", + ext_str, e + )) + .yellow() + ); + } + } + } + + for (idx, ext_str) in streamable_http_extensions.iter().enumerate() { + let config = CliSession::parse_streamable_http_extension(ext_str); + let hint = truncate_with_ellipsis(ext_str, EXTENSION_HINT_MAX_LEN); + let label = format!("http #{}({})", idx + 1, hint); + extensions_to_load.push((label, config)); + } + + for builtin_str in builtins { + let configs = CliSession::parse_builtin_extensions(builtin_str); + for config in configs { + extensions_to_load.push((config.name(), config)); + } + } + + extensions_to_load +} + /// Configuration for building a new Goose session /// /// This struct contains all the parameters needed to create a new session, @@ -447,10 +503,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { eprintln!("{}", style(format!("Warning: {}", warning)).yellow()); } - // If we get extensions_override, only run those extensions and none other - let extensions_to_run: Vec<_> = if let Some(extensions) = session_config.extensions_override { - extensions.into_iter().collect() - } else if session_config.resume { + let configured_extensions: Vec = if session_config.resume { match SessionManager::get_session(&session_id, false).await { Ok(session_data) => { if let Some(saved_state) = @@ -467,60 +520,84 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { } _ => get_enabled_extensions(), } + } else if let Some(extensions) = session_config.extensions_override.clone() { + extensions } else { get_enabled_extensions() }; + let cli_flag_extension_extensions_to_load = parse_cli_flag_extensions( + &session_config.extensions, + &session_config.streamable_http_extensions, + &session_config.builtins, + ); + + let mut extensions_to_load: Vec<(String, ExtensionConfig)> = configured_extensions + .iter() + .map(|cfg| (cfg.name(), cfg.clone())) + .collect(); + extensions_to_load.extend(cli_flag_extension_extensions_to_load); + let mut set = JoinSet::new(); let agent_ptr = Arc::new(agent); - let mut waiting_on = HashSet::new(); - for extension in extensions_to_run { - waiting_on.insert(extension.name()); + let mut waiting_ids: BTreeSet = (0..extensions_to_load.len()).collect(); + for (id, (_label, extension)) in extensions_to_load.iter().enumerate() { let agent_ptr = agent_ptr.clone(); - set.spawn(async move { - ( - extension.name(), - agent_ptr.add_extension(extension.clone()).await, - ) - }); + let cfg = extension.clone(); + set.spawn(async move { (id, agent_ptr.add_extension(cfg).await) }); } - let get_message = |waiting_on: &HashSet| { - let mut names: Vec<_> = waiting_on.iter().cloned().collect(); - names.sort(); - format!("starting {} extensions: {}", names.len(), names.join(", ")) + let get_message = |waiting_ids: &BTreeSet| { + let labels: Vec = waiting_ids + .iter() + .map(|id| { + extensions_to_load + .get(*id) + .map(|e| e.0.clone()) + .unwrap_or_default() + }) + .collect(); + format!( + "starting {} extensions: {}", + waiting_ids.len(), + labels.join(", ") + ) }; let spinner = cliclack::spinner(); - spinner.start(get_message(&waiting_on)); + spinner.start(get_message(&waiting_ids)); - let mut offer_debug = Vec::new(); + let mut offer_debug: Vec<(usize, anyhow::Error)> = Vec::new(); while let Some(result) = set.join_next().await { match result { - Ok((name, Ok(_))) => { - waiting_on.remove(&name); - spinner.set_message(get_message(&waiting_on)); + Ok((id, Ok(_))) => { + waiting_ids.remove(&id); + spinner.set_message(get_message(&waiting_ids)); } - Ok((name, Err(e))) => offer_debug.push((name, e)), + Ok((id, Err(e))) => offer_debug.push((id, e.into())), Err(e) => tracing::error!("failed to add extension: {}", e), } } spinner.clear(); - for (name, err) in offer_debug { + for (id, err) in offer_debug { + let label = extensions_to_load + .get(id) + .map(|e| e.0.clone()) + .unwrap_or_default(); eprintln!( "{}", style(format!( "Warning: Failed to start extension '{}' ({}), continuing without it", - name, err + label, err )) .yellow() ); if let Err(debug_err) = offer_extension_debugging_help( - &name, + &label, &err.to_string(), Arc::clone(&provider_for_display), session_config.interactive, @@ -547,7 +624,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { let debug_mode = session_config.debug || config.get_param("GOOSE_DEBUG").unwrap_or(false); // Create new session - let mut session = CliSession::new( + let session = CliSession::new( Arc::try_unwrap(agent_ptr).unwrap_or_else(|_| panic!("There should be no more references")), session_id.clone(), debug_mode, @@ -559,100 +636,12 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { ) .await; - // Add stdio extensions if provided - for extension_str in session_config.extensions { - if let Err(e) = session.add_extension(extension_str.clone()).await { - eprintln!( - "{}", - style(format!( - "Warning: Failed to start stdio extension '{}' ({}), continuing without it", - extension_str, e - )) - .yellow() - ); - - // Offer debugging help - if let Err(debug_err) = offer_extension_debugging_help( - &extension_str, - &e.to_string(), - Arc::clone(&provider_for_display), - session_config.interactive, - ) - .await - { - eprintln!("Note: Could not start debugging session: {}", debug_err); - } - } - } - - // Add streamable HTTP extensions if provided - for extension_str in session_config.streamable_http_extensions { - if let Err(e) = session - .add_streamable_http_extension(extension_str.clone()) - .await - { - eprintln!( - "{}", - style(format!( - "Warning: Failed to start streamable HTTP extension '{}' ({}), continuing without it", - extension_str, e - )) - .yellow() - ); - - // Offer debugging help - if let Err(debug_err) = offer_extension_debugging_help( - &extension_str, - &e.to_string(), - Arc::clone(&provider_for_display), - session_config.interactive, - ) - .await - { - eprintln!("Note: Could not start debugging session: {}", debug_err); - } - } - } - - // Add builtin extensions - for builtin in session_config.builtins { - if let Err(e) = session.add_builtin(builtin.clone()).await { - eprintln!( - "{}", - style(format!( - "Warning: Failed to start builtin extension '{}' ({}), continuing without it", - builtin, e - )) - .yellow() - ); - - // Offer debugging help - if let Err(debug_err) = offer_extension_debugging_help( - &builtin, - &e.to_string(), - Arc::clone(&provider_for_display), - session_config.interactive, - ) - .await - { - eprintln!("Note: Could not start debugging session: {}", debug_err); - } - } - } - - let session_config_for_save = SessionConfig { - id: session_id.clone(), - schedule_id: None, - max_turns: None, - retry_config: None, - }; - if let Err(e) = session .agent - .save_extension_state(&session_config_for_save) + .persist_extension_state(&session_id.clone()) .await { - tracing::warn!("Failed to save initial extension state: {}", e); + tracing::warn!("Failed to save extension state: {}", e); } // Add CLI-specific system prompt extension @@ -762,4 +751,24 @@ mod tests { assert_eq!(extension_name, "test-extension"); assert_eq!(error_message, "test error"); } + + #[test] + fn test_truncate_with_ellipsis() { + // String shorter than max length - no truncation + assert_eq!(truncate_with_ellipsis("abc", 5), "abc"); + + // String exactly at max length - no truncation + assert_eq!(truncate_with_ellipsis("abcde", 5), "abcde"); + + // String longer than max length - truncated with ellipsis + assert_eq!(truncate_with_ellipsis("abcdef", 5), "abcde…"); + assert_eq!(truncate_with_ellipsis("hello world", 5), "hello…"); + + // Empty string + assert_eq!(truncate_with_ellipsis("", 5), ""); + + // Unicode characters (each emoji is one char) + assert_eq!(truncate_with_ellipsis("πŸŽ‰πŸŽŠπŸŽˆ", 5), "πŸŽ‰πŸŽŠπŸŽˆ"); + assert_eq!(truncate_with_ellipsis("πŸŽ‰πŸŽŠπŸŽˆπŸŽπŸŽ€πŸŽ„", 5), "πŸŽ‰πŸŽŠπŸŽˆπŸŽπŸŽ€β€¦"); + } } diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index 0c671e3ace7e..01e29ced75c3 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -253,12 +253,9 @@ impl CliSession { &self.session_id } - /// Add a stdio extension to the session - /// - /// # Arguments - /// * `extension_command` - Full command string including environment variables - /// Format: "ENV1=val1 ENV2=val2 command args..." - pub async fn add_extension(&mut self, extension_command: String) -> Result<()> { + /// Parse a stdio extension command string into an ExtensionConfig + /// Format: "ENV1=val1 ENV2=val2 command args..." + pub fn parse_stdio_extension(extension_command: &str) -> Result { let mut parts: Vec<&str> = extension_command.split_whitespace().collect(); let mut envs = HashMap::new(); @@ -277,91 +274,117 @@ impl CliSession { let cmd = parts.remove(0).to_string(); - let config = ExtensionConfig::Stdio { + Ok(ExtensionConfig::Stdio { name: String::new(), cmd, args: parts.iter().map(|s| s.to_string()).collect(), envs: Envs::new(envs), env_keys: Vec::new(), description: goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string(), - // TODO: should set timeout timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT), bundled: None, available_tools: Vec::new(), - }; - - self.agent - .add_extension(config) - .await - .map_err(|e| anyhow::anyhow!("Failed to start extension: {}", e))?; - - // Invalidate the completion cache when a new extension is added - self.invalidate_completion_cache().await; - - Ok(()) + }) } - /// Add a streamable HTTP extension to the session - /// - /// # Arguments - /// * `extension_url` - URL of the server - pub async fn add_streamable_http_extension(&mut self, extension_url: String) -> Result<()> { - let config = ExtensionConfig::StreamableHttp { + /// Parse a streamable HTTP extension URL into an ExtensionConfig + pub fn parse_streamable_http_extension(extension_url: &str) -> ExtensionConfig { + ExtensionConfig::StreamableHttp { name: String::new(), - uri: extension_url, + uri: extension_url.to_string(), envs: Envs::new(HashMap::new()), env_keys: Vec::new(), headers: HashMap::new(), description: goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string(), - // TODO: should set timeout timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT), bundled: None, available_tools: Vec::new(), - }; + } + } + /// Parse builtin extension names (comma-separated) into ExtensionConfigs + pub fn parse_builtin_extensions(builtin_name: &str) -> Vec { + builtin_name + .split(',') + .map(|name| { + let extension_name = name.trim(); + if PLATFORM_EXTENSIONS.contains_key(extension_name) { + ExtensionConfig::Platform { + name: extension_name.to_string(), + bundled: None, + description: extension_name.to_string(), + available_tools: Vec::new(), + } + } else { + ExtensionConfig::Builtin { + name: extension_name.to_string(), + display_name: None, + timeout: None, + bundled: None, + description: extension_name.to_string(), + available_tools: Vec::new(), + } + } + }) + .collect() + } + + /// Add extension config, persist to session, and invalidate cache + async fn add_and_persist_extension(&mut self, config: ExtensionConfig) -> Result<()> { self.agent .add_extension(config) .await .map_err(|e| anyhow::anyhow!("Failed to start extension: {}", e))?; + // Save extension state to session (like Desktop) + self.agent + .persist_extension_state(&self.session_id) + .await + .map_err(|e| anyhow::anyhow!("Failed to save extension state: {}", e))?; + // Invalidate the completion cache when a new extension is added self.invalidate_completion_cache().await; Ok(()) } + /// Add a stdio extension to the session + /// + /// # Arguments + /// * `extension_command` - Full command string including environment variables + /// Format: "ENV1=val1 ENV2=val2 command args..." + pub async fn add_extension(&mut self, extension_command: String) -> Result<()> { + let config = Self::parse_stdio_extension(&extension_command)?; + self.add_and_persist_extension(config).await + } + + /// Add a streamable HTTP extension to the session + /// + /// # Arguments + /// * `extension_url` - URL of the server + pub async fn add_streamable_http_extension(&mut self, extension_url: String) -> Result<()> { + let config = Self::parse_streamable_http_extension(&extension_url); + self.add_and_persist_extension(config).await + } + /// Add a builtin extension to the session /// /// # Arguments /// * `builtin_name` - Name of the builtin extension(s), comma separated pub async fn add_builtin(&mut self, builtin_name: String) -> Result<()> { - for name in builtin_name.split(',') { - let extension_name = name.trim(); - - let config = if PLATFORM_EXTENSIONS.contains_key(extension_name) { - ExtensionConfig::Platform { - name: extension_name.to_string(), - bundled: None, - description: name.to_string(), - available_tools: Vec::new(), - } - } else { - ExtensionConfig::Builtin { - name: extension_name.to_string(), - display_name: None, - timeout: None, - bundled: None, - description: name.to_string(), - available_tools: Vec::new(), - } - }; + for config in Self::parse_builtin_extensions(&builtin_name) { self.agent .add_extension(config) .await .map_err(|e| anyhow::anyhow!("Failed to start builtin extension: {}", e))?; } - // Invalidate the completion cache when a new extension is added + // Save once after all builtins are added + self.agent + .persist_extension_state(&self.session_id) + .await + .map_err(|e| anyhow::anyhow!("Failed to save extension state: {}", e))?; + self.invalidate_completion_cache().await; Ok(()) From 14e5fcc293ccec4ee061bae3b2060569bb8c4dd3 Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Tue, 13 Jan 2026 22:54:34 +1100 Subject: [PATCH 02/12] create resolve exensions function as soure of truth (WIP) --- crates/goose-cli/src/session/builder.rs | 40 +++--- crates/goose-server/src/routes/agent.rs | 13 +- .../goose/src/session/extension_resolver.rs | 120 ++++++++++++++++++ crates/goose/src/session/mod.rs | 2 + 4 files changed, 149 insertions(+), 26 deletions(-) create mode 100644 crates/goose/src/session/extension_resolver.rs diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs index 659296013813..cd44fa20f096 100644 --- a/crates/goose-cli/src/session/builder.rs +++ b/crates/goose-cli/src/session/builder.rs @@ -5,14 +5,15 @@ use goose::agents::extension::PlatformExtensionContext; use goose::agents::types::RetryConfig; use goose::agents::Agent; use goose::config::{ - extensions::get_extension_by_name, get_all_extensions, get_enabled_extensions, Config, - ExtensionConfig, + extensions::get_extension_by_name, get_all_extensions, Config, ExtensionConfig, }; use goose::providers::create; use goose::recipe::{Response, SubRecipe}; use goose::session::session_manager::SessionType; use goose::session::SessionManager; -use goose::session::{EnabledExtensionsState, ExtensionState}; +use goose::session::{ + resolve_extensions, EnabledExtensionsState, ExtensionResolutionStrategy, ExtensionState, +}; use rustyline::EditMode; use std::collections::BTreeSet; use std::process; @@ -504,26 +505,21 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { } let configured_extensions: Vec = if session_config.resume { - match SessionManager::get_session(&session_id, false).await { - Ok(session_data) => { - if let Some(saved_state) = - EnabledExtensionsState::from_extension_data(&session_data.extension_data) - { - check_missing_extensions_or_exit( - &saved_state.extensions, - session_config.interactive, - ); - saved_state.extensions - } else { - get_enabled_extensions() - } - } - _ => get_enabled_extensions(), - } - } else if let Some(extensions) = session_config.extensions_override.clone() { - extensions + let session_exts = SessionManager::get_session(&session_id, false) + .await + .ok() + .and_then(|s| EnabledExtensionsState::from_extension_data(&s.extension_data)) + .map(|state| { + check_missing_extensions_or_exit(&state.extensions, session_config.interactive); + state.extensions + }); + resolve_extensions(ExtensionResolutionStrategy::Resume, None, session_exts) } else { - get_enabled_extensions() + resolve_extensions( + ExtensionResolutionStrategy::RecipeFirst, + session_config.extensions_override.as_ref(), + None, + ) }; let cli_flag_extension_extensions_to_load = parse_cli_flag_extensions( diff --git a/crates/goose-server/src/routes/agent.rs b/crates/goose-server/src/routes/agent.rs index e4c150c3ff3b..2171feb42da6 100644 --- a/crates/goose-server/src/routes/agent.rs +++ b/crates/goose-server/src/routes/agent.rs @@ -23,7 +23,10 @@ use goose::recipe::Recipe; use goose::recipe_deeplink; use goose::session::extension_data::ExtensionState; use goose::session::session_manager::SessionType; -use goose::session::{EnabledExtensionsState, Session, SessionManager}; +use goose::session::{ + resolve_extensions, EnabledExtensionsState, ExtensionResolutionStrategy, Session, + SessionManager, +}; use goose::{ agents::{extension::ToolInfo, extension_manager::get_parameter_names}, config::permission::PermissionLevel, @@ -220,9 +223,11 @@ async fn start_agent( } })?; - // Initialize session with extensions (either overrides from hub or global defaults) - let extensions_to_use = - extension_overrides.unwrap_or_else(goose::config::get_enabled_extensions); + // Initialize session with extensions (either overrides from hub, recipe extensions, or global defaults) + let recipe_extensions = original_recipe.as_ref().and_then(|r| r.extensions.as_ref()); + let extensions_to_use = extension_overrides.unwrap_or_else(|| { + resolve_extensions(ExtensionResolutionStrategy::RecipeFirst, recipe_extensions, None) + }); let mut extension_data = session.extension_data.clone(); let extensions_state = EnabledExtensionsState::new(extensions_to_use); if let Err(e) = extensions_state.to_extension_data(&mut extension_data) { diff --git a/crates/goose/src/session/extension_resolver.rs b/crates/goose/src/session/extension_resolver.rs new file mode 100644 index 000000000000..e07aa1a0fa2a --- /dev/null +++ b/crates/goose/src/session/extension_resolver.rs @@ -0,0 +1,120 @@ +use crate::config::{get_enabled_extensions, ExtensionConfig}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExtensionResolutionStrategy { + RecipeFirst, + Resume, +} + +/// Resolves which extensions to load for a session. +/// +/// Priority order: +/// - Resume strategy: Use session's saved extensions, fallback to global config +/// - RecipeFirst strategy: Use recipe extensions if present, fallback to global config +/// +/// # Arguments +/// * `strategy` - The resolution strategy to use +/// * `recipe_extensions` - Extensions defined in the recipe (if any) +/// * `session_extensions` - Extensions saved in the session (for resume scenarios) +/// +/// # Returns +/// The list of extensions to load +pub fn resolve_extensions( + strategy: ExtensionResolutionStrategy, + recipe_extensions: Option<&Vec>, + session_extensions: Option>, +) -> Vec { + match strategy { + ExtensionResolutionStrategy::Resume => { + session_extensions.unwrap_or_else(get_enabled_extensions) + } + ExtensionResolutionStrategy::RecipeFirst => recipe_extensions + .cloned() + .unwrap_or_else(get_enabled_extensions), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::ExtensionConfig; + + fn create_test_extension(name: &str) -> ExtensionConfig { + ExtensionConfig::Builtin { + name: name.to_string(), + } + } + + #[test] + fn test_recipe_first_with_recipe_extensions() { + let recipe_exts = vec![ + create_test_extension("recipe_ext_1"), + create_test_extension("recipe_ext_2"), + ]; + + let result = resolve_extensions( + ExtensionResolutionStrategy::RecipeFirst, + Some(&recipe_exts), + None, + ); + + assert_eq!(result.len(), 2); + assert_eq!(result[0].name(), "recipe_ext_1"); + assert_eq!(result[1].name(), "recipe_ext_2"); + } + + #[test] + fn test_recipe_first_without_recipe_extensions_falls_back_to_global() { + // When no recipe extensions, should fall back to global config + // This test just verifies the fallback path is taken + let result = resolve_extensions(ExtensionResolutionStrategy::RecipeFirst, None, None); + + // Result will be from get_enabled_extensions() which depends on config + // We just verify it doesn't panic and returns a Vec + assert!(result.len() >= 0); + } + + #[test] + fn test_resume_with_session_extensions() { + let session_exts = vec![ + create_test_extension("session_ext_1"), + create_test_extension("session_ext_2"), + ]; + + let result = resolve_extensions( + ExtensionResolutionStrategy::Resume, + None, + Some(session_exts), + ); + + assert_eq!(result.len(), 2); + assert_eq!(result[0].name(), "session_ext_1"); + assert_eq!(result[1].name(), "session_ext_2"); + } + + #[test] + fn test_resume_ignores_recipe_extensions() { + let recipe_exts = vec![create_test_extension("recipe_ext")]; + let session_exts = vec![create_test_extension("session_ext")]; + + // Even with recipe extensions provided, Resume strategy should use session extensions + let result = resolve_extensions( + ExtensionResolutionStrategy::Resume, + Some(&recipe_exts), + Some(session_exts), + ); + + assert_eq!(result.len(), 1); + assert_eq!(result[0].name(), "session_ext"); + } + + #[test] + fn test_resume_without_session_extensions_falls_back_to_global() { + // When no session extensions, should fall back to global config + let result = resolve_extensions(ExtensionResolutionStrategy::Resume, None, None); + + // Result will be from get_enabled_extensions() which depends on config + // We just verify it doesn't panic and returns a Vec + assert!(result.len() >= 0); + } +} diff --git a/crates/goose/src/session/mod.rs b/crates/goose/src/session/mod.rs index 28a1768c1701..1ce26426552e 100644 --- a/crates/goose/src/session/mod.rs +++ b/crates/goose/src/session/mod.rs @@ -1,9 +1,11 @@ mod chat_history_search; mod diagnostics; pub mod extension_data; +mod extension_resolver; mod legacy; pub mod session_manager; pub use diagnostics::{generate_diagnostics, get_system_info, SystemInfo}; pub use extension_data::{EnabledExtensionsState, ExtensionData, ExtensionState, TodoState}; +pub use extension_resolver::{resolve_extensions, ExtensionResolutionStrategy}; pub use session_manager::{Session, SessionInsights, SessionManager, SessionType}; From 8c7196dcca61c5559231fba90a3e945359772322 Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Wed, 14 Jan 2026 20:05:05 +1100 Subject: [PATCH 03/12] changed the extension resolve signature --- crates/goose-cli/src/session/builder.rs | 17 +-- crates/goose-server/src/routes/agent.rs | 12 +- .../goose/src/session/extension_resolver.rs | 108 ++++++------------ crates/goose/src/session/mod.rs | 2 +- 4 files changed, 47 insertions(+), 92 deletions(-) diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs index cd44fa20f096..760bcb45ea4f 100644 --- a/crates/goose-cli/src/session/builder.rs +++ b/crates/goose-cli/src/session/builder.rs @@ -11,9 +11,8 @@ use goose::providers::create; use goose::recipe::{Response, SubRecipe}; use goose::session::session_manager::SessionType; use goose::session::SessionManager; -use goose::session::{ - resolve_extensions, EnabledExtensionsState, ExtensionResolutionStrategy, ExtensionState, -}; +use goose::config::get_enabled_extensions; +use goose::session::{resolve_extensions_for_new_session, EnabledExtensionsState, ExtensionState}; use rustyline::EditMode; use std::collections::BTreeSet; use std::process; @@ -505,21 +504,17 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { } let configured_extensions: Vec = if session_config.resume { - let session_exts = SessionManager::get_session(&session_id, false) + SessionManager::get_session(&session_id, false) .await .ok() .and_then(|s| EnabledExtensionsState::from_extension_data(&s.extension_data)) .map(|state| { check_missing_extensions_or_exit(&state.extensions, session_config.interactive); state.extensions - }); - resolve_extensions(ExtensionResolutionStrategy::Resume, None, session_exts) + }) + .unwrap_or_else(get_enabled_extensions) } else { - resolve_extensions( - ExtensionResolutionStrategy::RecipeFirst, - session_config.extensions_override.as_ref(), - None, - ) + resolve_extensions_for_new_session(session_config.extensions_override.as_deref(), None) }; let cli_flag_extension_extensions_to_load = parse_cli_flag_extensions( diff --git a/crates/goose-server/src/routes/agent.rs b/crates/goose-server/src/routes/agent.rs index 2171feb42da6..84d9a509b87e 100644 --- a/crates/goose-server/src/routes/agent.rs +++ b/crates/goose-server/src/routes/agent.rs @@ -24,8 +24,7 @@ use goose::recipe_deeplink; use goose::session::extension_data::ExtensionState; use goose::session::session_manager::SessionType; use goose::session::{ - resolve_extensions, EnabledExtensionsState, ExtensionResolutionStrategy, Session, - SessionManager, + resolve_extensions_for_new_session, EnabledExtensionsState, Session, SessionManager, }; use goose::{ agents::{extension::ToolInfo, extension_manager::get_parameter_names}, @@ -223,11 +222,10 @@ async fn start_agent( } })?; - // Initialize session with extensions (either overrides from hub, recipe extensions, or global defaults) - let recipe_extensions = original_recipe.as_ref().and_then(|r| r.extensions.as_ref()); - let extensions_to_use = extension_overrides.unwrap_or_else(|| { - resolve_extensions(ExtensionResolutionStrategy::RecipeFirst, recipe_extensions, None) - }); + // Initialize session with extensions (recipe extensions β†’ hub overrides β†’ global defaults) + let recipe_extensions = original_recipe.as_ref().and_then(|r| r.extensions.as_deref()); + let extensions_to_use = + resolve_extensions_for_new_session(recipe_extensions, extension_overrides); let mut extension_data = session.extension_data.clone(); let extensions_state = EnabledExtensionsState::new(extensions_to_use); if let Err(e) = extensions_state.to_extension_data(&mut extension_data) { diff --git a/crates/goose/src/session/extension_resolver.rs b/crates/goose/src/session/extension_resolver.rs index e07aa1a0fa2a..f5edf70c844d 100644 --- a/crates/goose/src/session/extension_resolver.rs +++ b/crates/goose/src/session/extension_resolver.rs @@ -1,37 +1,28 @@ use crate::config::{get_enabled_extensions, ExtensionConfig}; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ExtensionResolutionStrategy { - RecipeFirst, - Resume, -} - -/// Resolves which extensions to load for a session. +/// Resolves which extensions to load for a new session. /// /// Priority order: -/// - Resume strategy: Use session's saved extensions, fallback to global config -/// - RecipeFirst strategy: Use recipe extensions if present, fallback to global config +/// 1. Recipe extensions (if defined in recipe) +/// 2. Override extensions (if provided) +/// 3. Global config (fallback) /// /// # Arguments -/// * `strategy` - The resolution strategy to use /// * `recipe_extensions` - Extensions defined in the recipe (if any) -/// * `session_extensions` - Extensions saved in the session (for resume scenarios) -/// -/// # Returns -/// The list of extensions to load -pub fn resolve_extensions( - strategy: ExtensionResolutionStrategy, - recipe_extensions: Option<&Vec>, - session_extensions: Option>, +/// * `override_extensions` - Extensions provided as overrides (e.g., from hub) +pub fn resolve_extensions_for_new_session( + recipe_extensions: Option<&[ExtensionConfig]>, + override_extensions: Option>, ) -> Vec { - match strategy { - ExtensionResolutionStrategy::Resume => { - session_extensions.unwrap_or_else(get_enabled_extensions) - } - ExtensionResolutionStrategy::RecipeFirst => recipe_extensions - .cloned() - .unwrap_or_else(get_enabled_extensions), + if let Some(exts) = recipe_extensions { + return exts.to_vec(); } + + if let Some(exts) = override_extensions { + return exts; + } + + get_enabled_extensions() } #[cfg(test)] @@ -42,21 +33,24 @@ mod tests { fn create_test_extension(name: &str) -> ExtensionConfig { ExtensionConfig::Builtin { name: name.to_string(), + display_name: None, + description: String::new(), + timeout: None, + bundled: None, + available_tools: Vec::new(), } } #[test] - fn test_recipe_first_with_recipe_extensions() { + fn test_recipe_extensions_take_priority() { let recipe_exts = vec![ create_test_extension("recipe_ext_1"), create_test_extension("recipe_ext_2"), ]; + let override_exts = vec![create_test_extension("override_ext")]; - let result = resolve_extensions( - ExtensionResolutionStrategy::RecipeFirst, - Some(&recipe_exts), - None, - ); + let result = + resolve_extensions_for_new_session(Some(&recipe_exts), Some(override_exts)); assert_eq!(result.len(), 2); assert_eq!(result[0].name(), "recipe_ext_1"); @@ -64,57 +58,25 @@ mod tests { } #[test] - fn test_recipe_first_without_recipe_extensions_falls_back_to_global() { - // When no recipe extensions, should fall back to global config - // This test just verifies the fallback path is taken - let result = resolve_extensions(ExtensionResolutionStrategy::RecipeFirst, None, None); - - // Result will be from get_enabled_extensions() which depends on config - // We just verify it doesn't panic and returns a Vec - assert!(result.len() >= 0); - } - - #[test] - fn test_resume_with_session_extensions() { - let session_exts = vec![ - create_test_extension("session_ext_1"), - create_test_extension("session_ext_2"), + fn test_override_extensions_used_when_no_recipe() { + let override_exts = vec![ + create_test_extension("override_ext_1"), + create_test_extension("override_ext_2"), ]; - let result = resolve_extensions( - ExtensionResolutionStrategy::Resume, - None, - Some(session_exts), - ); + let result = resolve_extensions_for_new_session(None, Some(override_exts)); assert_eq!(result.len(), 2); - assert_eq!(result[0].name(), "session_ext_1"); - assert_eq!(result[1].name(), "session_ext_2"); - } - - #[test] - fn test_resume_ignores_recipe_extensions() { - let recipe_exts = vec![create_test_extension("recipe_ext")]; - let session_exts = vec![create_test_extension("session_ext")]; - - // Even with recipe extensions provided, Resume strategy should use session extensions - let result = resolve_extensions( - ExtensionResolutionStrategy::Resume, - Some(&recipe_exts), - Some(session_exts), - ); - - assert_eq!(result.len(), 1); - assert_eq!(result[0].name(), "session_ext"); + assert_eq!(result[0].name(), "override_ext_1"); + assert_eq!(result[1].name(), "override_ext_2"); } #[test] - fn test_resume_without_session_extensions_falls_back_to_global() { - // When no session extensions, should fall back to global config - let result = resolve_extensions(ExtensionResolutionStrategy::Resume, None, None); + fn test_falls_back_to_global_when_no_recipe_or_override() { + let result = resolve_extensions_for_new_session(None, None); // Result will be from get_enabled_extensions() which depends on config // We just verify it doesn't panic and returns a Vec - assert!(result.len() >= 0); + let _ = result; } } diff --git a/crates/goose/src/session/mod.rs b/crates/goose/src/session/mod.rs index 1ce26426552e..12945d2451f8 100644 --- a/crates/goose/src/session/mod.rs +++ b/crates/goose/src/session/mod.rs @@ -7,5 +7,5 @@ pub mod session_manager; pub use diagnostics::{generate_diagnostics, get_system_info, SystemInfo}; pub use extension_data::{EnabledExtensionsState, ExtensionData, ExtensionState, TodoState}; -pub use extension_resolver::{resolve_extensions, ExtensionResolutionStrategy}; +pub use extension_resolver::resolve_extensions_for_new_session; pub use session_manager::{Session, SessionInsights, SessionManager, SessionType}; From 33051cf55b4a321edbbc56da3320cca94ed8c5e1 Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Wed, 14 Jan 2026 20:05:31 +1100 Subject: [PATCH 04/12] move extension_override to RecipeInfo --- crates/goose-cli/src/cli.rs | 7 ++----- crates/goose-cli/src/recipes/extract_from_cli.rs | 6 +++--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index 62d1e55e985e..4025a766d9b9 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -931,12 +931,12 @@ enum CliProviderVariant { #[derive(Debug)] pub struct InputConfig { pub contents: Option, - pub extensions_override: Option>, pub additional_system_prompt: Option, } #[derive(Debug)] pub struct RecipeInfo { + pub extensions: Option>, pub session_settings: Option, pub sub_recipes: Option>, pub final_output_response: Option, @@ -1153,7 +1153,6 @@ fn parse_run_input( Ok(Some(( InputConfig { contents: Some(contents), - extensions_override: None, additional_system_prompt: input_opts.system.clone(), }, None, @@ -1170,7 +1169,6 @@ fn parse_run_input( Ok(Some(( InputConfig { contents: Some(contents), - extensions_override: None, additional_system_prompt: None, }, None, @@ -1179,7 +1177,6 @@ fn parse_run_input( (_, Some(text), _) => Ok(Some(( InputConfig { contents: Some(text.clone()), - extensions_override: None, additional_system_prompt: input_opts.system.clone(), }, None, @@ -1274,7 +1271,7 @@ async fn handle_run_command( extensions: extension_opts.extensions, streamable_http_extensions: extension_opts.streamable_http_extensions, builtins: extension_opts.builtins, - extensions_override: input_config.extensions_override, + extensions_override: recipe_info.as_ref().and_then(|r| r.extensions.clone()), additional_system_prompt: input_config.additional_system_prompt, settings: recipe_info .as_ref() diff --git a/crates/goose-cli/src/recipes/extract_from_cli.rs b/crates/goose-cli/src/recipes/extract_from_cli.rs index 02546c0f785b..a10fc035f106 100644 --- a/crates/goose-cli/src/recipes/extract_from_cli.rs +++ b/crates/goose-cli/src/recipes/extract_from_cli.rs @@ -52,11 +52,11 @@ pub fn extract_recipe_info_from_cli( } let input_config = InputConfig { contents: recipe.prompt.filter(|s| !s.trim().is_empty()), - extensions_override: recipe.extensions, additional_system_prompt: recipe.instructions, }; let recipe_info = RecipeInfo { + extensions: recipe.extensions, session_settings: recipe.settings.map(|s| SessionSettings { goose_provider: s.goose_provider, goose_model: s.goose_model, @@ -109,7 +109,7 @@ mod tests { input_config.additional_system_prompt, Some("test_instructions my_value".to_string()) ); - assert!(input_config.extensions_override.is_none()); + assert!(recipe_info.extensions.is_none()); assert!(settings.is_some()); let settings = settings.unwrap(); @@ -174,7 +174,7 @@ mod tests { input_config.additional_system_prompt, Some("test_instructions my_value".to_string()) ); - assert!(input_config.extensions_override.is_none()); + assert!(recipe_info.extensions.is_none()); assert!(settings.is_some()); let settings = settings.unwrap(); From 4cccfa3df9979addd54070906253eb2869b85d20 Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Wed, 14 Jan 2026 20:56:06 +1100 Subject: [PATCH 05/12] removed intermediate structs such as RecipeInfo, SessionSettings and pass Recipe directly to SessionBuilderConfig --- crates/goose-cli/src/cli.rs | 45 +++------ crates/goose-cli/src/commands/bench.rs | 6 +- .../goose-cli/src/recipes/extract_from_cli.rs | 55 +++++------ crates/goose-cli/src/session/builder.rs | 91 ++++++++----------- crates/goose-cli/src/session/mod.rs | 2 +- 5 files changed, 74 insertions(+), 125 deletions(-) diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index 4025a766d9b9..1daa11471998 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -1,7 +1,8 @@ use anyhow::Result; use clap::{Args, CommandFactory, Parser, Subcommand}; use clap_complete::{generate, Shell as ClapShell}; -use goose::config::{Config, ExtensionConfig}; +use goose::config::Config; +use goose::recipe::Recipe; use goose_mcp::mcp_server_runner::{serve, McpCommand}; use goose_mcp::{ AutoVisualiserRouter, ComputerControllerServer, DeveloperServer, MemoryServer, TutorialServer, @@ -25,7 +26,7 @@ use crate::commands::schedule::{ use crate::commands::session::{handle_session_list, handle_session_remove}; use crate::recipes::extract_from_cli::extract_recipe_info_from_cli; use crate::recipes::recipe::{explain_recipe, render_recipe_as_yaml}; -use crate::session::{build_session, SessionBuilderConfig, SessionSettings}; +use crate::session::{build_session, SessionBuilderConfig}; use goose::session::session_manager::SessionType; use goose::session::SessionManager; use goose_bench::bench_config::BenchRunConfig; @@ -934,14 +935,6 @@ pub struct InputConfig { pub additional_system_prompt: Option, } -#[derive(Debug)] -pub struct RecipeInfo { - pub extensions: Option>, - pub session_settings: Option, - pub sub_recipes: Option>, - pub final_output_response: Option, - pub retry_config: Option, -} fn get_command_name(command: &Option) -> &'static str { match command { @@ -1069,9 +1062,8 @@ async fn handle_interactive_session( extensions: extension_opts.extensions, streamable_http_extensions: extension_opts.streamable_http_extensions, builtins: extension_opts.builtins, - extensions_override: None, + recipe: None, additional_system_prompt: None, - settings: None, provider: None, model: None, debug: session_opts.debug, @@ -1080,9 +1072,6 @@ async fn handle_interactive_session( scheduled_job_id: None, interactive: true, quiet: false, - sub_recipes: None, - final_output_response: None, - retry_config: None, output_format: "text".to_string(), }) .await; @@ -1139,7 +1128,7 @@ async fn log_session_completion( fn parse_run_input( input_opts: &InputOptions, quiet: bool, -) -> Result)>> { +) -> Result)>> { match ( &input_opts.instructions, &input_opts.input_text, @@ -1220,13 +1209,13 @@ fn parse_run_input( "Recipe execution started" ); - let (input_config, recipe_info) = extract_recipe_info_from_cli( + let (input_config, recipe) = extract_recipe_info_from_cli( recipe_name.clone(), input_opts.params.clone(), input_opts.additional_sub_recipes.clone(), quiet, )?; - Ok(Some((input_config, Some(recipe_info)))) + Ok(Some((input_config, Some(recipe)))) } (None, None, None) => { eprintln!("Error: Must provide either --instructions (-i), --text (-t), or --recipe. Use -i - for stdin."); @@ -1246,7 +1235,7 @@ async fn handle_run_command( ) -> Result<()> { let parsed = parse_run_input(&input_opts, output_opts.quiet)?; - let Some((input_config, recipe_info)) = parsed else { + let Some((input_config, recipe)) = parsed else { return Ok(()); }; @@ -1271,11 +1260,8 @@ async fn handle_run_command( extensions: extension_opts.extensions, streamable_http_extensions: extension_opts.streamable_http_extensions, builtins: extension_opts.builtins, - extensions_override: recipe_info.as_ref().and_then(|r| r.extensions.clone()), + recipe: recipe.clone(), additional_system_prompt: input_config.additional_system_prompt, - settings: recipe_info - .as_ref() - .and_then(|r| r.session_settings.clone()), provider: model_opts.provider, model: model_opts.model, debug: session_opts.debug, @@ -1284,11 +1270,6 @@ async fn handle_run_command( scheduled_job_id: run_behavior.scheduled_job_id, interactive: run_behavior.interactive, quiet: output_opts.quiet, - sub_recipes: recipe_info.as_ref().and_then(|r| r.sub_recipes.clone()), - final_output_response: recipe_info - .as_ref() - .and_then(|r| r.final_output_response.clone()), - retry_config: recipe_info.as_ref().and_then(|r| r.retry_config.clone()), output_format: output_opts.output_format, }) .await; @@ -1297,7 +1278,7 @@ async fn handle_run_command( session.interactive(input_config.contents).await } else if let Some(contents) = input_config.contents { let session_start = std::time::Instant::now(); - let session_type = if recipe_info.is_some() { + let session_type = if recipe.is_some() { "recipe" } else { "run" @@ -1403,9 +1384,8 @@ async fn handle_default_session() -> Result<()> { extensions: Vec::new(), streamable_http_extensions: Vec::new(), builtins: Vec::new(), - extensions_override: None, + recipe: None, additional_system_prompt: None, - settings: None::, provider: None, model: None, debug: false, @@ -1414,9 +1394,6 @@ async fn handle_default_session() -> Result<()> { scheduled_job_id: None, interactive: true, quiet: false, - sub_recipes: None, - final_output_response: None, - retry_config: None, output_format: "text".to_string(), }) .await; diff --git a/crates/goose-cli/src/commands/bench.rs b/crates/goose-cli/src/commands/bench.rs index 10bd42539d29..6f1cc7104311 100644 --- a/crates/goose-cli/src/commands/bench.rs +++ b/crates/goose-cli/src/commands/bench.rs @@ -40,9 +40,8 @@ pub async fn agent_generator( extensions: requirements.external, streamable_http_extensions: requirements.streamable_http, builtins: requirements.builtin, - extensions_override: None, + recipe: None, additional_system_prompt: None, - settings: None, provider: None, model: None, debug: false, @@ -51,9 +50,6 @@ pub async fn agent_generator( scheduled_job_id: None, max_turns: None, quiet: false, - sub_recipes: None, - final_output_response: None, - retry_config: None, output_format: "text".to_string(), }) .await; diff --git a/crates/goose-cli/src/recipes/extract_from_cli.rs b/crates/goose-cli/src/recipes/extract_from_cli.rs index a10fc035f106..ca4e9b2b48c1 100644 --- a/crates/goose-cli/src/recipes/extract_from_cli.rs +++ b/crates/goose-cli/src/recipes/extract_from_cli.rs @@ -1,31 +1,30 @@ use std::path::PathBuf; use anyhow::{anyhow, Result}; -use goose::recipe::SubRecipe; +use goose::recipe::{Recipe, SubRecipe}; +use crate::cli::InputConfig; use crate::recipes::print_recipe::print_recipe_info; use crate::recipes::recipe::load_recipe; use crate::recipes::search_recipe::load_recipe_file; -use crate::{ - cli::{InputConfig, RecipeInfo}, - session::SessionSettings, -}; pub fn extract_recipe_info_from_cli( recipe_name: String, params: Vec<(String, String)>, additional_sub_recipes: Vec, quiet: bool, -) -> Result<(InputConfig, RecipeInfo)> { - let recipe = load_recipe(&recipe_name, params.clone()).unwrap_or_else(|err| { +) -> Result<(InputConfig, Recipe)> { + let mut recipe = load_recipe(&recipe_name, params.clone()).unwrap_or_else(|err| { eprintln!("{}: {}", console::style("Error").red().bold(), err); std::process::exit(1); }); if !quiet { print_recipe_info(&recipe, params); } - let mut all_sub_recipes = recipe.sub_recipes.clone().unwrap_or_default(); + + // Add additional sub-recipes from CLI to the recipe if !additional_sub_recipes.is_empty() { + let mut all_sub_recipes = recipe.sub_recipes.clone().unwrap_or_default(); for sub_recipe_name in additional_sub_recipes { match load_recipe_file(&sub_recipe_name) { Ok(recipe_file) => { @@ -49,25 +48,15 @@ pub fn extract_recipe_info_from_cli( } } } + recipe.sub_recipes = Some(all_sub_recipes); } - let input_config = InputConfig { - contents: recipe.prompt.filter(|s| !s.trim().is_empty()), - additional_system_prompt: recipe.instructions, - }; - let recipe_info = RecipeInfo { - extensions: recipe.extensions, - session_settings: recipe.settings.map(|s| SessionSettings { - goose_provider: s.goose_provider, - goose_model: s.goose_model, - temperature: s.temperature, - }), - sub_recipes: Some(all_sub_recipes), - final_output_response: recipe.response, - retry_config: recipe.retry, + let input_config = InputConfig { + contents: recipe.prompt.clone().filter(|s| !s.trim().is_empty()), + additional_system_prompt: recipe.instructions.clone(), }; - Ok((input_config, recipe_info)) + Ok((input_config, recipe)) } fn extract_recipe_name(recipe_identifier: &str) -> String { @@ -98,18 +87,18 @@ mod tests { let params = vec![("name".to_string(), "my_value".to_string())]; let recipe_name = recipe_path.to_str().unwrap().to_string(); - let (input_config, recipe_info) = + let (input_config, recipe) = extract_recipe_info_from_cli(recipe_name, params, Vec::new(), false).unwrap(); - let settings = recipe_info.session_settings; - let sub_recipes = recipe_info.sub_recipes; - let response = recipe_info.final_output_response; + let settings = recipe.settings; + let sub_recipes = recipe.sub_recipes; + let response = recipe.response; assert_eq!(input_config.contents, Some("test_prompt".to_string())); assert_eq!( input_config.additional_system_prompt, Some("test_instructions my_value".to_string()) ); - assert!(recipe_info.extensions.is_none()); + assert!(recipe.extensions.is_none()); assert!(settings.is_some()); let settings = settings.unwrap(); @@ -162,19 +151,19 @@ mod tests { sub_recipe2_path.to_string_lossy().to_string(), ]; - let (input_config, recipe_info) = + let (input_config, recipe) = extract_recipe_info_from_cli(recipe_name, params, additional_sub_recipes, false) .unwrap(); - let settings = recipe_info.session_settings; - let sub_recipes = recipe_info.sub_recipes; - let response = recipe_info.final_output_response; + let settings = recipe.settings; + let sub_recipes = recipe.sub_recipes; + let response = recipe.response; assert_eq!(input_config.contents, Some("test_prompt".to_string())); assert_eq!( input_config.additional_system_prompt, Some("test_instructions my_value".to_string()) ); - assert!(recipe_info.extensions.is_none()); + assert!(recipe.extensions.is_none()); assert!(settings.is_some()); let settings = settings.unwrap(); diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs index 760bcb45ea4f..a9c090449e95 100644 --- a/crates/goose-cli/src/session/builder.rs +++ b/crates/goose-cli/src/session/builder.rs @@ -2,13 +2,12 @@ use super::output; use super::CliSession; use console::style; use goose::agents::extension::PlatformExtensionContext; -use goose::agents::types::RetryConfig; use goose::agents::Agent; use goose::config::{ extensions::get_extension_by_name, get_all_extensions, Config, ExtensionConfig, }; use goose::providers::create; -use goose::recipe::{Response, SubRecipe}; +use goose::recipe::Recipe; use goose::session::session_manager::SessionType; use goose::session::SessionManager; use goose::config::get_enabled_extensions; @@ -94,12 +93,10 @@ pub struct SessionBuilderConfig { pub streamable_http_extensions: Vec, /// List of builtin extension commands to add pub builtins: Vec, - /// List of extensions to enable, enable only this set and ignore configured ones - pub extensions_override: Option>, + /// Recipe configuration for the session + pub recipe: Option, /// Any additional system prompt to append to the default pub additional_system_prompt: Option, - /// Settings to override the global Goose settings - pub settings: Option, /// Provider override from CLI arguments pub provider: Option, /// Model override from CLI arguments @@ -116,12 +113,6 @@ pub struct SessionBuilderConfig { pub interactive: bool, /// Quiet mode - suppress non-response output pub quiet: bool, - /// Sub-recipes to add to the session - pub sub_recipes: Option>, - /// Final output expected response - pub final_output_response: Option, - /// Retry configuration for automated validation and recovery - pub retry_config: Option, /// Output format (text, json) pub output_format: String, } @@ -137,9 +128,8 @@ impl Default for SessionBuilderConfig { extensions: Vec::new(), streamable_http_extensions: Vec::new(), builtins: Vec::new(), - extensions_override: None, + recipe: None, additional_system_prompt: None, - settings: None, provider: None, model: None, debug: false, @@ -148,9 +138,6 @@ impl Default for SessionBuilderConfig { scheduled_job_id: None, interactive: false, quiet: false, - sub_recipes: None, - final_output_response: None, - retry_config: None, output_format: "text".to_string(), } } @@ -297,13 +284,6 @@ fn check_missing_extensions_or_exit(saved_extensions: &[ExtensionConfig], intera } } -#[derive(Clone, Debug, Default)] -pub struct SessionSettings { - pub goose_model: Option, - pub goose_provider: Option, - pub temperature: Option, -} - pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { goose::posthog::set_session_context("cli", session_config.resume); @@ -322,27 +302,23 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { (None, None) }; + // Extract recipe settings for provider/model/temperature + let recipe_settings = session_config + .recipe + .as_ref() + .and_then(|r| r.settings.as_ref()); + let provider_name = session_config .provider .or(saved_provider) - .or_else(|| { - session_config - .settings - .as_ref() - .and_then(|s| s.goose_provider.clone()) - }) + .or_else(|| recipe_settings.and_then(|s| s.goose_provider.clone())) .or_else(|| config.get_goose_provider().ok()) .expect("No provider configured. Run 'goose configure' first"); let model_name = session_config .model .or_else(|| saved_model_config.as_ref().map(|mc| mc.model_name.clone())) - .or_else(|| { - session_config - .settings - .as_ref() - .and_then(|s| s.goose_model.clone()) - }) + .or_else(|| recipe_settings.and_then(|s| s.goose_model.clone())) .or_else(|| config.get_goose_model().ok()) .expect("No model configured. Run 'goose configure' first"); @@ -352,12 +328,12 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { .is_some_and(|mc| mc.model_name == model_name) { let mut config = saved_model_config.unwrap(); - if let Some(temp) = session_config.settings.as_ref().and_then(|s| s.temperature) { + if let Some(temp) = recipe_settings.and_then(|s| s.temperature) { config = config.with_temperature(Some(temp)); } config } else { - let temperature = session_config.settings.as_ref().and_then(|s| s.temperature); + let temperature = recipe_settings.and_then(|s| s.temperature); goose::model::ModelConfig::new(&model_name) .unwrap_or_else(|e| { output::render_error(&format!("Failed to create model configuration: {}", e)); @@ -368,12 +344,18 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { let agent: Agent = Agent::new(); + // Extract recipe components for agent + let sub_recipes = session_config + .recipe + .as_ref() + .and_then(|r| r.sub_recipes.clone()); + let final_output_response = session_config + .recipe + .as_ref() + .and_then(|r| r.response.clone()); + agent - .apply_recipe_components( - session_config.sub_recipes, - session_config.final_output_response, - true, - ) + .apply_recipe_components(sub_recipes, final_output_response, true) .await; let new_provider = match create(&provider_name, model_config).await { @@ -514,7 +496,11 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { }) .unwrap_or_else(get_enabled_extensions) } else { - resolve_extensions_for_new_session(session_config.extensions_override.as_deref(), None) + let recipe_extensions = session_config + .recipe + .as_ref() + .and_then(|r| r.extensions.as_deref()); + resolve_extensions_for_new_session(recipe_extensions, None) }; let cli_flag_extension_extensions_to_load = parse_cli_flag_extensions( @@ -614,6 +600,12 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { let debug_mode = session_config.debug || config.get_param("GOOSE_DEBUG").unwrap_or(false); + // Extract retry config from recipe + let retry_config = session_config + .recipe + .as_ref() + .and_then(|r| r.retry.clone()); + // Create new session let session = CliSession::new( Arc::try_unwrap(agent_ptr).unwrap_or_else(|_| panic!("There should be no more references")), @@ -622,7 +614,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { session_config.scheduled_job_id.clone(), session_config.max_turns, edit_mode, - session_config.retry_config.clone(), + retry_config, session_config.output_format.clone(), ) .await; @@ -679,9 +671,8 @@ mod tests { extensions: vec!["echo test".to_string()], streamable_http_extensions: vec!["http://localhost:8080/mcp".to_string()], builtins: vec!["developer".to_string()], - extensions_override: None, + recipe: None, additional_system_prompt: Some("Test prompt".to_string()), - settings: None, provider: None, model: None, debug: true, @@ -690,9 +681,6 @@ mod tests { scheduled_job_id: None, interactive: true, quiet: false, - sub_recipes: None, - final_output_response: None, - retry_config: None, output_format: "text".to_string(), }; @@ -717,7 +705,7 @@ mod tests { assert!(config.extensions.is_empty()); assert!(config.streamable_http_extensions.is_empty()); assert!(config.builtins.is_empty()); - assert!(config.extensions_override.is_none()); + assert!(config.recipe.is_none()); assert!(config.additional_system_prompt.is_none()); assert!(!config.debug); assert!(config.max_tool_repetitions.is_none()); @@ -725,7 +713,6 @@ mod tests { assert!(config.scheduled_job_id.is_none()); assert!(!config.interactive); assert!(!config.quiet); - assert!(config.final_output_response.is_none()); } #[tokio::test] diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index 01e29ced75c3..3b2cf04534e2 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -18,7 +18,7 @@ use tokio::signal::ctrl_c; use tokio_util::task::AbortOnDropHandle; pub use self::export::message_to_markdown; -pub use builder::{build_session, SessionBuilderConfig, SessionSettings}; +pub use builder::{build_session, SessionBuilderConfig}; use console::Color; use goose::agents::AgentEvent; use goose::permission::permission_confirmation::PrincipalType; From 52310103708a1abe04f2b94c7ff5880ae306ddb7 Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Wed, 14 Jan 2026 21:16:27 +1100 Subject: [PATCH 06/12] cleanup --- crates/goose-cli/src/cli.rs | 7 +-- .../goose-cli/src/recipes/extract_from_cli.rs | 1 - crates/goose-cli/src/session/builder.rs | 19 ++----- crates/goose-cli/src/session/mod.rs | 51 ++++--------------- crates/goose-server/src/routes/agent.rs | 5 +- .../goose/src/session/extension_resolver.rs | 22 +------- 6 files changed, 19 insertions(+), 86 deletions(-) diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index 1daa11471998..ed38e397fbd6 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -935,7 +935,6 @@ pub struct InputConfig { pub additional_system_prompt: Option, } - fn get_command_name(command: &Option) -> &'static str { match command { Some(Command::Configure {}) => "configure", @@ -1278,11 +1277,7 @@ async fn handle_run_command( session.interactive(input_config.contents).await } else if let Some(contents) = input_config.contents { let session_start = std::time::Instant::now(); - let session_type = if recipe.is_some() { - "recipe" - } else { - "run" - }; + let session_type = if recipe.is_some() { "recipe" } else { "run" }; tracing::info!( counter.goose.session_starts = 1, diff --git a/crates/goose-cli/src/recipes/extract_from_cli.rs b/crates/goose-cli/src/recipes/extract_from_cli.rs index ca4e9b2b48c1..aac9e91cea8b 100644 --- a/crates/goose-cli/src/recipes/extract_from_cli.rs +++ b/crates/goose-cli/src/recipes/extract_from_cli.rs @@ -22,7 +22,6 @@ pub fn extract_recipe_info_from_cli( print_recipe_info(&recipe, params); } - // Add additional sub-recipes from CLI to the recipe if !additional_sub_recipes.is_empty() { let mut all_sub_recipes = recipe.sub_recipes.clone().unwrap_or_default(); for sub_recipe_name in additional_sub_recipes { diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs index a9c090449e95..9e6b42af59db 100644 --- a/crates/goose-cli/src/session/builder.rs +++ b/crates/goose-cli/src/session/builder.rs @@ -3,6 +3,7 @@ use super::CliSession; use console::style; use goose::agents::extension::PlatformExtensionContext; use goose::agents::Agent; +use goose::config::get_enabled_extensions; use goose::config::{ extensions::get_extension_by_name, get_all_extensions, Config, ExtensionConfig, }; @@ -10,7 +11,6 @@ use goose::providers::create; use goose::recipe::Recipe; use goose::session::session_manager::SessionType; use goose::session::SessionManager; -use goose::config::get_enabled_extensions; use goose::session::{resolve_extensions_for_new_session, EnabledExtensionsState, ExtensionState}; use rustyline::EditMode; use std::collections::BTreeSet; @@ -18,10 +18,8 @@ use std::process; use std::sync::Arc; use tokio::task::JoinSet; -/// Maximum length for extension hint display in spinner messages const EXTENSION_HINT_MAX_LEN: usize = 5; -/// Truncates a string to a maximum length, appending an ellipsis if truncated. fn truncate_with_ellipsis(s: &str, max_len: usize) -> String { let truncated: String = s.chars().take(max_len).collect(); if s.chars().count() > max_len { @@ -93,7 +91,7 @@ pub struct SessionBuilderConfig { pub streamable_http_extensions: Vec, /// List of builtin extension commands to add pub builtins: Vec, - /// Recipe configuration for the session + /// Recipe for the session pub recipe: Option, /// Any additional system prompt to append to the default pub additional_system_prompt: Option, @@ -302,7 +300,6 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { (None, None) }; - // Extract recipe settings for provider/model/temperature let recipe_settings = session_config .recipe .as_ref() @@ -344,7 +341,6 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { let agent: Agent = Agent::new(); - // Extract recipe components for agent let sub_recipes = session_config .recipe .as_ref() @@ -601,12 +597,8 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { let debug_mode = session_config.debug || config.get_param("GOOSE_DEBUG").unwrap_or(false); // Extract retry config from recipe - let retry_config = session_config - .recipe - .as_ref() - .and_then(|r| r.retry.clone()); + let retry_config = session_config.recipe.as_ref().and_then(|r| r.retry.clone()); - // Create new session let session = CliSession::new( Arc::try_unwrap(agent_ptr).unwrap_or_else(|_| panic!("There should be no more references")), session_id.clone(), @@ -732,20 +724,15 @@ mod tests { #[test] fn test_truncate_with_ellipsis() { - // String shorter than max length - no truncation assert_eq!(truncate_with_ellipsis("abc", 5), "abc"); - // String exactly at max length - no truncation assert_eq!(truncate_with_ellipsis("abcde", 5), "abcde"); - // String longer than max length - truncated with ellipsis assert_eq!(truncate_with_ellipsis("abcdef", 5), "abcde…"); assert_eq!(truncate_with_ellipsis("hello world", 5), "hello…"); - // Empty string assert_eq!(truncate_with_ellipsis("", 5), ""); - // Unicode characters (each emoji is one char) assert_eq!(truncate_with_ellipsis("πŸŽ‰πŸŽŠπŸŽˆ", 5), "πŸŽ‰πŸŽŠπŸŽˆ"); assert_eq!(truncate_with_ellipsis("πŸŽ‰πŸŽŠπŸŽˆπŸŽπŸŽ€πŸŽ„", 5), "πŸŽ‰πŸŽŠπŸŽˆπŸŽπŸŽ€β€¦"); } diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index 3b2cf04534e2..612d00f92c3b 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -287,7 +287,6 @@ impl CliSession { }) } - /// Parse a streamable HTTP extension URL into an ExtensionConfig pub fn parse_streamable_http_extension(extension_url: &str) -> ExtensionConfig { ExtensionConfig::StreamableHttp { name: String::new(), @@ -329,65 +328,37 @@ impl CliSession { .collect() } - /// Add extension config, persist to session, and invalidate cache - async fn add_and_persist_extension(&mut self, config: ExtensionConfig) -> Result<()> { - self.agent - .add_extension(config) - .await - .map_err(|e| anyhow::anyhow!("Failed to start extension: {}", e))?; + async fn add_and_persist_extensions(&mut self, configs: Vec) -> Result<()> { + for config in configs { + self.agent + .add_extension(config) + .await + .map_err(|e| anyhow::anyhow!("Failed to start extension: {}", e))?; + } - // Save extension state to session (like Desktop) self.agent .persist_extension_state(&self.session_id) .await .map_err(|e| anyhow::anyhow!("Failed to save extension state: {}", e))?; - // Invalidate the completion cache when a new extension is added self.invalidate_completion_cache().await; Ok(()) } - /// Add a stdio extension to the session - /// - /// # Arguments - /// * `extension_command` - Full command string including environment variables - /// Format: "ENV1=val1 ENV2=val2 command args..." pub async fn add_extension(&mut self, extension_command: String) -> Result<()> { let config = Self::parse_stdio_extension(&extension_command)?; - self.add_and_persist_extension(config).await + self.add_and_persist_extensions(vec![config]).await } - /// Add a streamable HTTP extension to the session - /// - /// # Arguments - /// * `extension_url` - URL of the server pub async fn add_streamable_http_extension(&mut self, extension_url: String) -> Result<()> { let config = Self::parse_streamable_http_extension(&extension_url); - self.add_and_persist_extension(config).await + self.add_and_persist_extensions(vec![config]).await } - /// Add a builtin extension to the session - /// - /// # Arguments - /// * `builtin_name` - Name of the builtin extension(s), comma separated pub async fn add_builtin(&mut self, builtin_name: String) -> Result<()> { - for config in Self::parse_builtin_extensions(&builtin_name) { - self.agent - .add_extension(config) - .await - .map_err(|e| anyhow::anyhow!("Failed to start builtin extension: {}", e))?; - } - - // Save once after all builtins are added - self.agent - .persist_extension_state(&self.session_id) - .await - .map_err(|e| anyhow::anyhow!("Failed to save extension state: {}", e))?; - - self.invalidate_completion_cache().await; - - Ok(()) + let configs = Self::parse_builtin_extensions(&builtin_name); + self.add_and_persist_extensions(configs).await } pub async fn list_prompts( diff --git a/crates/goose-server/src/routes/agent.rs b/crates/goose-server/src/routes/agent.rs index 84d9a509b87e..8f5cdd6db644 100644 --- a/crates/goose-server/src/routes/agent.rs +++ b/crates/goose-server/src/routes/agent.rs @@ -222,8 +222,9 @@ async fn start_agent( } })?; - // Initialize session with extensions (recipe extensions β†’ hub overrides β†’ global defaults) - let recipe_extensions = original_recipe.as_ref().and_then(|r| r.extensions.as_deref()); + let recipe_extensions = original_recipe + .as_ref() + .and_then(|r| r.extensions.as_deref()); let extensions_to_use = resolve_extensions_for_new_session(recipe_extensions, extension_overrides); let mut extension_data = session.extension_data.clone(); diff --git a/crates/goose/src/session/extension_resolver.rs b/crates/goose/src/session/extension_resolver.rs index f5edf70c844d..8720ef9a2db0 100644 --- a/crates/goose/src/session/extension_resolver.rs +++ b/crates/goose/src/session/extension_resolver.rs @@ -1,15 +1,5 @@ use crate::config::{get_enabled_extensions, ExtensionConfig}; -/// Resolves which extensions to load for a new session. -/// -/// Priority order: -/// 1. Recipe extensions (if defined in recipe) -/// 2. Override extensions (if provided) -/// 3. Global config (fallback) -/// -/// # Arguments -/// * `recipe_extensions` - Extensions defined in the recipe (if any) -/// * `override_extensions` - Extensions provided as overrides (e.g., from hub) pub fn resolve_extensions_for_new_session( recipe_extensions: Option<&[ExtensionConfig]>, override_extensions: Option>, @@ -49,8 +39,7 @@ mod tests { ]; let override_exts = vec![create_test_extension("override_ext")]; - let result = - resolve_extensions_for_new_session(Some(&recipe_exts), Some(override_exts)); + let result = resolve_extensions_for_new_session(Some(&recipe_exts), Some(override_exts)); assert_eq!(result.len(), 2); assert_eq!(result[0].name(), "recipe_ext_1"); @@ -70,13 +59,4 @@ mod tests { assert_eq!(result[0].name(), "override_ext_1"); assert_eq!(result[1].name(), "override_ext_2"); } - - #[test] - fn test_falls_back_to_global_when_no_recipe_or_override() { - let result = resolve_extensions_for_new_session(None, None); - - // Result will be from get_enabled_extensions() which depends on config - // We just verify it doesn't panic and returns a Vec - let _ = result; - } } From bbf9f3a283ee2c3cad12fe6b5afebe1c856b6494 Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Wed, 14 Jan 2026 21:30:08 +1100 Subject: [PATCH 07/12] more refactoring --- crates/goose-cli/src/session/builder.rs | 190 ++++++++++++------------ 1 file changed, 95 insertions(+), 95 deletions(-) diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs index 9e6b42af59db..e4cff26d27e2 100644 --- a/crates/goose-cli/src/session/builder.rs +++ b/crates/goose-cli/src/session/builder.rs @@ -243,6 +243,85 @@ async fn offer_extension_debugging_help( Ok(()) } +async fn load_extensions( + agent: Agent, + extensions_to_load: Vec<(String, ExtensionConfig)>, + provider_for_debug: Arc, + interactive: bool, +) -> Arc { + let mut set = JoinSet::new(); + let agent_ptr = Arc::new(agent); + + let mut waiting_ids: BTreeSet = (0..extensions_to_load.len()).collect(); + for (id, (_label, extension)) in extensions_to_load.iter().enumerate() { + let agent_ptr = agent_ptr.clone(); + let cfg = extension.clone(); + set.spawn(async move { (id, agent_ptr.add_extension(cfg).await) }); + } + + let get_message = |waiting_ids: &BTreeSet| { + let labels: Vec = waiting_ids + .iter() + .map(|id| { + extensions_to_load + .get(*id) + .map(|e| e.0.clone()) + .unwrap_or_default() + }) + .collect(); + format!( + "starting {} extensions: {}", + waiting_ids.len(), + labels.join(", ") + ) + }; + + let spinner = cliclack::spinner(); + spinner.start(get_message(&waiting_ids)); + + let mut offer_debug: Vec<(usize, anyhow::Error)> = Vec::new(); + while let Some(result) = set.join_next().await { + match result { + Ok((id, Ok(_))) => { + waiting_ids.remove(&id); + spinner.set_message(get_message(&waiting_ids)); + } + Ok((id, Err(e))) => offer_debug.push((id, e.into())), + Err(e) => tracing::error!("failed to add extension: {}", e), + } + } + + spinner.clear(); + + for (id, err) in offer_debug { + let label = extensions_to_load + .get(id) + .map(|e| e.0.clone()) + .unwrap_or_default(); + eprintln!( + "{}", + style(format!( + "Warning: Failed to start extension '{}' ({}), continuing without it", + label, err + )) + .yellow() + ); + + if let Err(debug_err) = offer_extension_debugging_help( + &label, + &err.to_string(), + Arc::clone(&provider_for_debug), + interactive, + ) + .await + { + eprintln!("Note: Could not start debugging session: {}", debug_err); + } + } + + agent_ptr +} + fn check_missing_extensions_or_exit(saved_extensions: &[ExtensionConfig], interactive: bool) { let missing: Vec<_> = saved_extensions .iter() @@ -300,10 +379,8 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { (None, None) }; - let recipe_settings = session_config - .recipe - .as_ref() - .and_then(|r| r.settings.as_ref()); + let recipe = session_config.recipe.as_ref(); + let recipe_settings = recipe.and_then(|r| r.settings.as_ref()); let provider_name = session_config .provider @@ -341,17 +418,12 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { let agent: Agent = Agent::new(); - let sub_recipes = session_config - .recipe - .as_ref() - .and_then(|r| r.sub_recipes.clone()); - let final_output_response = session_config - .recipe - .as_ref() - .and_then(|r| r.response.clone()); - agent - .apply_recipe_components(sub_recipes, final_output_response, true) + .apply_recipe_components( + recipe.and_then(|r| r.sub_recipes.clone()), + recipe.and_then(|r| r.response.clone()), + true, + ) .await; let new_provider = match create(&provider_name, model_config).await { @@ -492,11 +564,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { }) .unwrap_or_else(get_enabled_extensions) } else { - let recipe_extensions = session_config - .recipe - .as_ref() - .and_then(|r| r.extensions.as_deref()); - resolve_extensions_for_new_session(recipe_extensions, None) + resolve_extensions_for_new_session(recipe.and_then(|r| r.extensions.as_deref()), None) }; let cli_flag_extension_extensions_to_load = parse_cli_flag_extensions( @@ -511,75 +579,13 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { .collect(); extensions_to_load.extend(cli_flag_extension_extensions_to_load); - let mut set = JoinSet::new(); - let agent_ptr = Arc::new(agent); - - let mut waiting_ids: BTreeSet = (0..extensions_to_load.len()).collect(); - for (id, (_label, extension)) in extensions_to_load.iter().enumerate() { - let agent_ptr = agent_ptr.clone(); - let cfg = extension.clone(); - set.spawn(async move { (id, agent_ptr.add_extension(cfg).await) }); - } - - let get_message = |waiting_ids: &BTreeSet| { - let labels: Vec = waiting_ids - .iter() - .map(|id| { - extensions_to_load - .get(*id) - .map(|e| e.0.clone()) - .unwrap_or_default() - }) - .collect(); - format!( - "starting {} extensions: {}", - waiting_ids.len(), - labels.join(", ") - ) - }; - - let spinner = cliclack::spinner(); - spinner.start(get_message(&waiting_ids)); - - let mut offer_debug: Vec<(usize, anyhow::Error)> = Vec::new(); - while let Some(result) = set.join_next().await { - match result { - Ok((id, Ok(_))) => { - waiting_ids.remove(&id); - spinner.set_message(get_message(&waiting_ids)); - } - Ok((id, Err(e))) => offer_debug.push((id, e.into())), - Err(e) => tracing::error!("failed to add extension: {}", e), - } - } - - spinner.clear(); - - for (id, err) in offer_debug { - let label = extensions_to_load - .get(id) - .map(|e| e.0.clone()) - .unwrap_or_default(); - eprintln!( - "{}", - style(format!( - "Warning: Failed to start extension '{}' ({}), continuing without it", - label, err - )) - .yellow() - ); - - if let Err(debug_err) = offer_extension_debugging_help( - &label, - &err.to_string(), - Arc::clone(&provider_for_display), - session_config.interactive, - ) - .await - { - eprintln!("Note: Could not start debugging session: {}", debug_err); - } - } + let agent_ptr = load_extensions( + agent, + extensions_to_load, + Arc::clone(&provider_for_display), + session_config.interactive, + ) + .await; // Determine editor mode let edit_mode = config @@ -596,9 +602,6 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { let debug_mode = session_config.debug || config.get_param("GOOSE_DEBUG").unwrap_or(false); - // Extract retry config from recipe - let retry_config = session_config.recipe.as_ref().and_then(|r| r.retry.clone()); - let session = CliSession::new( Arc::try_unwrap(agent_ptr).unwrap_or_else(|_| panic!("There should be no more references")), session_id.clone(), @@ -606,7 +609,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { session_config.scheduled_job_id.clone(), session_config.max_turns, edit_mode, - retry_config, + recipe.and_then(|r| r.retry.clone()), session_config.output_format.clone(), ) .await; @@ -732,8 +735,5 @@ mod tests { assert_eq!(truncate_with_ellipsis("hello world", 5), "hello…"); assert_eq!(truncate_with_ellipsis("", 5), ""); - - assert_eq!(truncate_with_ellipsis("πŸŽ‰πŸŽŠπŸŽˆ", 5), "πŸŽ‰πŸŽŠπŸŽˆ"); - assert_eq!(truncate_with_ellipsis("πŸŽ‰πŸŽŠπŸŽˆπŸŽπŸŽ€πŸŽ„", 5), "πŸŽ‰πŸŽŠπŸŽˆπŸŽπŸŽ€β€¦"); } } From 838637d3b067939a15075b9f19ae55d5893c209c Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Wed, 14 Jan 2026 23:11:33 +1100 Subject: [PATCH 08/12] fmt --- crates/goose-cli/src/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index d344416c16c3..b7b8dfbaf6ee 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -2,8 +2,8 @@ use anyhow::Result; use clap::{Args, CommandFactory, Parser, Subcommand}; use clap_complete::{generate, Shell as ClapShell}; use goose::config::Config; -use goose::recipe::Recipe; use goose::posthog::get_telemetry_choice; +use goose::recipe::Recipe; use goose_mcp::mcp_server_runner::{serve, McpCommand}; use goose_mcp::{ AutoVisualiserRouter, ComputerControllerServer, DeveloperServer, MemoryServer, TutorialServer, From d75e48a6f91b7eaf9e63ed1e7f6cd2e814d81453 Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Wed, 14 Jan 2026 23:21:53 +1100 Subject: [PATCH 09/12] used extension resolver for scheduler --- crates/goose/src/scheduler.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/goose/src/scheduler.rs b/crates/goose/src/scheduler.rs index 3e7071da6a66..3032214ced98 100644 --- a/crates/goose/src/scheduler.rs +++ b/crates/goose/src/scheduler.rs @@ -22,6 +22,7 @@ use crate::posthog; use crate::providers::create; use crate::recipe::Recipe; use crate::scheduler_trait::SchedulerTrait; +use crate::session::resolve_extensions_for_new_session; use crate::session::session_manager::SessionType; use crate::session::{Session, SessionManager}; @@ -736,10 +737,9 @@ async fn execute_job( let agent_provider = create(&provider_name, model_config).await?; - if let Some(ref extensions) = recipe.extensions { - for ext in extensions { - agent.add_extension(ext.clone()).await?; - } + let extensions = resolve_extensions_for_new_session(recipe.extensions.as_deref(), None); + for ext in extensions { + agent.add_extension(ext.clone()).await?; } let session = SessionManager::create_session( From 27547b81a561c9932284ba44f816c229a5ad00f1 Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Thu, 15 Jan 2026 16:59:59 +1100 Subject: [PATCH 10/12] update the variable name --- crates/goose-cli/src/session/builder.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs index e4cff26d27e2..d1504bd085a4 100644 --- a/crates/goose-cli/src/session/builder.rs +++ b/crates/goose-cli/src/session/builder.rs @@ -567,7 +567,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { resolve_extensions_for_new_session(recipe.and_then(|r| r.extensions.as_deref()), None) }; - let cli_flag_extension_extensions_to_load = parse_cli_flag_extensions( + let cli_flag_extensions_to_load = parse_cli_flag_extensions( &session_config.extensions, &session_config.streamable_http_extensions, &session_config.builtins, @@ -577,7 +577,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { .iter() .map(|cfg| (cfg.name(), cfg.clone())) .collect(); - extensions_to_load.extend(cli_flag_extension_extensions_to_load); + extensions_to_load.extend(cli_flag_extensions_to_load); let agent_ptr = load_extensions( agent, From a5c57bafe14dd9fc9301e3ca1e63cead26742406 Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Thu, 15 Jan 2026 17:26:40 +1100 Subject: [PATCH 11/12] format --- crates/goose-cli/src/session/builder.rs | 7 ++++--- crates/goose-server/src/routes/agent.rs | 4 +--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs index dc0da83b8adc..b0d62a9a9c64 100644 --- a/crates/goose-cli/src/session/builder.rs +++ b/crates/goose-cli/src/session/builder.rs @@ -1,7 +1,6 @@ use super::output; use super::CliSession; use console::style; -use goose::agents::extension::PlatformExtensionContext; use goose::agents::Agent; use goose::config::get_enabled_extensions; use goose::config::{ @@ -10,7 +9,6 @@ use goose::config::{ use goose::providers::create; use goose::recipe::Recipe; use goose::session::session_manager::SessionType; -use goose::session::SessionManager; use goose::session::{resolve_extensions_for_new_session, EnabledExtensionsState, ExtensionState}; use rustyline::EditMode; use std::collections::BTreeSet; @@ -549,7 +547,10 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { } let configured_extensions: Vec = if session_config.resume { - agent.config.session_manager.get_session(&session_id, false) + agent + .config + .session_manager + .get_session(&session_id, false) .await .ok() .and_then(|s| EnabledExtensionsState::from_extension_data(&s.extension_data)) diff --git a/crates/goose-server/src/routes/agent.rs b/crates/goose-server/src/routes/agent.rs index 7058afe53cf6..e003cd0842f3 100644 --- a/crates/goose-server/src/routes/agent.rs +++ b/crates/goose-server/src/routes/agent.rs @@ -22,9 +22,7 @@ use goose::recipe::Recipe; use goose::recipe_deeplink; use goose::session::extension_data::ExtensionState; use goose::session::session_manager::SessionType; -use goose::session::{ - resolve_extensions_for_new_session, EnabledExtensionsState, Session, -}; +use goose::session::{resolve_extensions_for_new_session, EnabledExtensionsState, Session}; use goose::{ agents::{extension::ToolInfo, extension_manager::get_parameter_names}, config::permission::PermissionLevel, From 28c7fbb1feb00e0479463ffef6d260111850a885 Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Mon, 19 Jan 2026 10:38:38 +1100 Subject: [PATCH 12/12] moved to extension_resolver.rs to config extensins mod --- crates/goose-cli/src/session/builder.rs | 3 +- crates/goose-server/src/routes/agent.rs | 3 +- crates/goose/src/config/extensions.rs | 15 +++++ crates/goose/src/config/mod.rs | 4 +- crates/goose/src/scheduler.rs | 3 +- .../goose/src/session/extension_resolver.rs | 62 ------------------- crates/goose/src/session/mod.rs | 2 - 7 files changed, 22 insertions(+), 70 deletions(-) delete mode 100644 crates/goose/src/session/extension_resolver.rs diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs index b0d62a9a9c64..4ecc96ea38f0 100644 --- a/crates/goose-cli/src/session/builder.rs +++ b/crates/goose-cli/src/session/builder.rs @@ -3,13 +3,14 @@ use super::CliSession; use console::style; use goose::agents::Agent; use goose::config::get_enabled_extensions; +use goose::config::resolve_extensions_for_new_session; use goose::config::{ extensions::get_extension_by_name, get_all_extensions, Config, ExtensionConfig, }; use goose::providers::create; use goose::recipe::Recipe; use goose::session::session_manager::SessionType; -use goose::session::{resolve_extensions_for_new_session, EnabledExtensionsState, ExtensionState}; +use goose::session::{EnabledExtensionsState, ExtensionState}; use rustyline::EditMode; use std::collections::BTreeSet; use std::process; diff --git a/crates/goose-server/src/routes/agent.rs b/crates/goose-server/src/routes/agent.rs index e003cd0842f3..ef487eec1b17 100644 --- a/crates/goose-server/src/routes/agent.rs +++ b/crates/goose-server/src/routes/agent.rs @@ -14,6 +14,7 @@ use goose::agents::ExtensionLoadResult; use base64::Engine; use goose::agents::ExtensionConfig; +use goose::config::resolve_extensions_for_new_session; use goose::config::{Config, GooseMode}; use goose::model::ModelConfig; use goose::prompt_template::render_global_file; @@ -22,7 +23,7 @@ use goose::recipe::Recipe; use goose::recipe_deeplink; use goose::session::extension_data::ExtensionState; use goose::session::session_manager::SessionType; -use goose::session::{resolve_extensions_for_new_session, EnabledExtensionsState, Session}; +use goose::session::{EnabledExtensionsState, Session}; use goose::{ agents::{extension::ToolInfo, extension_manager::get_parameter_names}, config::permission::PermissionLevel, diff --git a/crates/goose/src/config/extensions.rs b/crates/goose/src/config/extensions.rs index 10e9d5dc67bc..c9f41f4ea353 100644 --- a/crates/goose/src/config/extensions.rs +++ b/crates/goose/src/config/extensions.rs @@ -155,3 +155,18 @@ pub fn get_warnings() -> Vec { } warnings } + +pub fn resolve_extensions_for_new_session( + recipe_extensions: Option<&[ExtensionConfig]>, + override_extensions: Option>, +) -> Vec { + if let Some(exts) = recipe_extensions { + return exts.to_vec(); + } + + if let Some(exts) = override_extensions { + return exts; + } + + get_enabled_extensions() +} diff --git a/crates/goose/src/config/mod.rs b/crates/goose/src/config/mod.rs index f7e3b8ad31b1..0599cfb8e43c 100644 --- a/crates/goose/src/config/mod.rs +++ b/crates/goose/src/config/mod.rs @@ -15,8 +15,8 @@ pub use declarative_providers::DeclarativeProviderConfig; pub use experiments::ExperimentManager; pub use extensions::{ get_all_extension_names, get_all_extensions, get_enabled_extensions, get_extension_by_name, - get_warnings, is_extension_enabled, remove_extension, set_extension, set_extension_enabled, - ExtensionEntry, + get_warnings, is_extension_enabled, remove_extension, resolve_extensions_for_new_session, + set_extension, set_extension_enabled, ExtensionEntry, }; pub use goose_mode::GooseMode; pub use permission::PermissionManager; diff --git a/crates/goose/src/scheduler.rs b/crates/goose/src/scheduler.rs index aa83e250360b..b4520d80d2a5 100644 --- a/crates/goose/src/scheduler.rs +++ b/crates/goose/src/scheduler.rs @@ -15,14 +15,13 @@ use tokio_util::sync::CancellationToken; use crate::agents::AgentEvent; use crate::agents::{Agent, SessionConfig}; use crate::config::paths::Paths; -use crate::config::Config; +use crate::config::{resolve_extensions_for_new_session, Config}; use crate::conversation::message::Message; use crate::conversation::Conversation; use crate::posthog; use crate::providers::create; use crate::recipe::Recipe; use crate::scheduler_trait::SchedulerTrait; -use crate::session::resolve_extensions_for_new_session; use crate::session::session_manager::SessionType; use crate::session::{Session, SessionManager}; diff --git a/crates/goose/src/session/extension_resolver.rs b/crates/goose/src/session/extension_resolver.rs deleted file mode 100644 index 8720ef9a2db0..000000000000 --- a/crates/goose/src/session/extension_resolver.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::config::{get_enabled_extensions, ExtensionConfig}; - -pub fn resolve_extensions_for_new_session( - recipe_extensions: Option<&[ExtensionConfig]>, - override_extensions: Option>, -) -> Vec { - if let Some(exts) = recipe_extensions { - return exts.to_vec(); - } - - if let Some(exts) = override_extensions { - return exts; - } - - get_enabled_extensions() -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::config::ExtensionConfig; - - fn create_test_extension(name: &str) -> ExtensionConfig { - ExtensionConfig::Builtin { - name: name.to_string(), - display_name: None, - description: String::new(), - timeout: None, - bundled: None, - available_tools: Vec::new(), - } - } - - #[test] - fn test_recipe_extensions_take_priority() { - let recipe_exts = vec![ - create_test_extension("recipe_ext_1"), - create_test_extension("recipe_ext_2"), - ]; - let override_exts = vec![create_test_extension("override_ext")]; - - let result = resolve_extensions_for_new_session(Some(&recipe_exts), Some(override_exts)); - - assert_eq!(result.len(), 2); - assert_eq!(result[0].name(), "recipe_ext_1"); - assert_eq!(result[1].name(), "recipe_ext_2"); - } - - #[test] - fn test_override_extensions_used_when_no_recipe() { - let override_exts = vec![ - create_test_extension("override_ext_1"), - create_test_extension("override_ext_2"), - ]; - - let result = resolve_extensions_for_new_session(None, Some(override_exts)); - - assert_eq!(result.len(), 2); - assert_eq!(result[0].name(), "override_ext_1"); - assert_eq!(result[1].name(), "override_ext_2"); - } -} diff --git a/crates/goose/src/session/mod.rs b/crates/goose/src/session/mod.rs index 1a8d6e3a2258..79eee530e142 100644 --- a/crates/goose/src/session/mod.rs +++ b/crates/goose/src/session/mod.rs @@ -1,13 +1,11 @@ mod chat_history_search; mod diagnostics; pub mod extension_data; -mod extension_resolver; mod legacy; pub mod session_manager; pub use diagnostics::{generate_diagnostics, get_system_info, SystemInfo}; pub use extension_data::{EnabledExtensionsState, ExtensionData, ExtensionState, TodoState}; -pub use extension_resolver::resolve_extensions_for_new_session; pub use session_manager::{ Session, SessionInsights, SessionManager, SessionType, SessionUpdateBuilder, };