diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index cd4241d76..f1b639d50 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -1,7 +1,7 @@ use cliclack::spinner; use console::style; use goose::agents::{extension::Envs, ExtensionConfig}; -use goose::config::{Config, ConfigError, ExtensionEntry, ExtensionManager}; +use goose::config::{Config, ConfigError, ExperimentManager, ExtensionEntry, ExtensionManager}; use goose::message::Message; use goose::providers::{create, providers}; use mcp_core::Tool; @@ -153,7 +153,7 @@ pub async fn handle_configure() -> Result<(), Box> { .item( "settings", "Goose Settings", - "Set the Goose Mode, Tool Output, and more", + "Set the Goose Mode, Tool Output, Experiment and more", ) .interact()?; @@ -622,14 +622,24 @@ pub fn remove_extension_dialog() -> Result<(), Box> { } pub fn configure_settings_dialog() -> Result<(), Box> { - let setting_type = cliclack::select("What setting would you like to configure?") + let mut setting_select_builder = cliclack::select("What setting would you like to configure?") .item("goose_mode", "Goose Mode", "Configure Goose mode") .item( "tool_output", "Tool Output", "Show more or less tool output", - ) - .interact()?; + ); + + // Conditionally add the "Toggle Experiment" option + if ExperimentManager::is_enabled("EXPERIMENT_CONFIG")? { + setting_select_builder = setting_select_builder.item( + "experiment", + "Toggle Experiment", + "Enable or disable an experiment feature", + ); + } + + let setting_type = setting_select_builder.interact()?; match setting_type { "goose_mode" => { @@ -638,6 +648,9 @@ pub fn configure_settings_dialog() -> Result<(), Box> { "tool_output" => { configure_tool_output_dialog()?; } + "experiment" => { + toggle_experiments_dialog()?; + } _ => unreachable!(), }; @@ -718,3 +731,43 @@ pub fn configure_tool_output_dialog() -> Result<(), Box> { Ok(()) } + +/// Configure experiment features that can be used with goose +/// Dialog for toggling which experiments are enabled/disabled +pub fn toggle_experiments_dialog() -> Result<(), Box> { + let experiments = ExperimentManager::get_all()?; + + if experiments.is_empty() { + cliclack::outro("No experiments supported yet.")?; + return Ok(()); + } + + // Get currently enabled experiments for the selection + let enabled_experiments: Vec<&String> = experiments + .iter() + .filter(|(_, enabled)| *enabled) + .map(|(name, _)| name) + .collect(); + + // Let user toggle experiments + let selected = cliclack::multiselect( + "enable experiments: (use \"space\" to toggle and \"enter\" to submit)", + ) + .required(false) + .items( + &experiments + .iter() + .map(|(name, _)| (name, name.as_str(), "")) + .collect::>(), + ) + .initial_values(enabled_experiments) + .interact()?; + + // Update enabled status for each experiments + for name in experiments.iter().map(|(name, _)| name) { + ExperimentManager::set_enabled(name, selected.iter().any(|&s| s.as_str() == name))?; + } + + cliclack::outro("Experiments settings updated successfully")?; + Ok(()) +} diff --git a/crates/goose/src/config/experiments.rs b/crates/goose/src/config/experiments.rs new file mode 100644 index 000000000..158ed3a56 --- /dev/null +++ b/crates/goose/src/config/experiments.rs @@ -0,0 +1,61 @@ +use super::base::Config; +use anyhow::Result; +use std::collections::HashMap; + +/// It is the ground truth for init experiments. The experiment names in users' experiment list but not +/// in the list will be remove from user list; The experiment names in the ground-truth list but not +/// in users' experiment list will be added to user list with default value false; +const ALL_EXPERIMENTS: &[(&str, bool)] = &[("EXPERIMENT_CONFIG", false)]; + +/// Experiment configuration management +pub struct ExperimentManager; + +impl ExperimentManager { + /// Get all experiments and their configurations + /// + /// - Ensures the user's experiment list is synchronized with `ALL_EXPERIMENTS`. + /// - Adds missing experiments from `ALL_EXPERIMENTS` with the default value. + /// - Removes experiments not in `ALL_EXPERIMENTS`. + pub fn get_all() -> Result> { + let config = Config::global(); + let mut experiments: HashMap = config.get("experiments").unwrap_or_default(); + + // Synchronize the user's experiments with the ground truth (`ALL_EXPERIMENTS`) + for &(key, default_value) in ALL_EXPERIMENTS { + experiments.entry(key.to_string()).or_insert(default_value); + } + + // Remove experiments not in `ALL_EXPERIMENTS` + experiments.retain(|key, _| ALL_EXPERIMENTS.iter().any(|(k, _)| k == key)); + + Ok(experiments.into_iter().collect()) + } + + /// Enable or disable an experiment + pub fn set_enabled(name: &str, enabled: bool) -> Result<()> { + let config = Config::global(); + + // Load existing experiments or initialize a new map + let mut experiments: HashMap = + config.get("experiments").unwrap_or_else(|_| HashMap::new()); + + // Update the status of the experiment + experiments.insert(name.to_string(), enabled); + + // Save the updated experiments map + config.set("experiments", serde_json::to_value(experiments)?)?; + Ok(()) + } + + /// Check if an experiment is enabled + pub fn is_enabled(name: &str) -> Result { + let config = Config::global(); + + // Load existing experiments or initialize a new map + let experiments: HashMap = + config.get("experiments").unwrap_or_else(|_| HashMap::new()); + + // Return whether the experiment is enabled, defaulting to false + Ok(*experiments.get(name).unwrap_or(&false)) + } +} diff --git a/crates/goose/src/config/mod.rs b/crates/goose/src/config/mod.rs index c72669a71..3c8091210 100644 --- a/crates/goose/src/config/mod.rs +++ b/crates/goose/src/config/mod.rs @@ -1,6 +1,8 @@ mod base; +mod experiments; mod extensions; pub use crate::agents::ExtensionConfig; pub use base::{Config, ConfigError, APP_STRATEGY}; +pub use experiments::ExperimentManager; pub use extensions::{ExtensionEntry, ExtensionManager};