Persist dynamic extension config so we can resume recipe sessions w/ extensions (#4331)

This commit is contained in:
Will Pfleger
2025-10-04 17:17:12 -04:00
committed by GitHub
parent 51651550e0
commit a59fbab69c
14 changed files with 357 additions and 305 deletions
+2 -3
View File
@@ -4,7 +4,7 @@ use agent_client_protocol::{
};
use anyhow::Result;
use goose::agents::Agent;
use goose::config::{Config, ExtensionConfigManager};
use goose::config::{get_all_extensions, Config};
use goose::conversation::message::{Message, MessageContent};
use goose::conversation::Conversation;
use goose::providers::create;
@@ -124,8 +124,7 @@ impl GooseAcpAgent {
agent.update_provider(provider.clone()).await?;
// Load and add extensions just like the normal CLI
let extensions_to_run: Vec<_> = ExtensionConfigManager::get_all()
.map_err(|e| anyhow::anyhow!("Failed to load extensions: {}", e))?
let extensions_to_run: Vec<_> = get_all_extensions()
.into_iter()
.filter(|ext| ext.enabled)
.map(|ext| ext.config)
+34 -44
View File
@@ -9,12 +9,12 @@ use goose::agents::platform_tools::{
use goose::agents::Agent;
use goose::agents::{extension::Envs, ExtensionConfig};
use goose::config::custom_providers::CustomProviderConfig;
use goose::config::extensions::name_to_key;
use goose::config::permission::PermissionLevel;
use goose::config::{
Config, ConfigError, ExperimentManager, ExtensionConfigManager, ExtensionEntry,
PermissionManager,
use goose::config::extensions::{
get_all_extension_names, get_all_extensions, get_enabled_extensions, get_extension_by_name,
name_to_key, remove_extension, set_extension, set_extension_enabled,
};
use goose::config::permission::PermissionLevel;
use goose::config::{Config, ConfigError, ExperimentManager, ExtensionEntry, PermissionManager};
use goose::conversation::message::Message;
use goose::model::ModelConfig;
use goose::providers::{create, providers};
@@ -105,10 +105,10 @@ pub async fn handle_configure() -> Result<(), Box<dyn Error>> {
);
// Since we are setting up for the first time, we'll also enable the developer system
// This operation is best-effort and errors are ignored
ExtensionConfigManager::set(ExtensionEntry {
set_extension(ExtensionEntry {
enabled: true,
config: ExtensionConfig::default(),
})?;
});
}
Ok(false) => {
let _ = config.clear();
@@ -641,7 +641,7 @@ pub async fn configure_provider_dialog() -> Result<bool, Box<dyn Error>> {
/// Configure extensions that can be used with goose
/// Dialog for toggling which extensions are enabled/disabled
pub fn toggle_extensions_dialog() -> Result<(), Box<dyn Error>> {
let extensions = ExtensionConfigManager::get_all()?;
let extensions = get_all_extensions();
if extensions.is_empty() {
cliclack::outro(
@@ -682,10 +682,10 @@ pub fn toggle_extensions_dialog() -> Result<(), Box<dyn Error>> {
// Update enabled status for each extension
for name in extension_status.iter().map(|(name, _)| name) {
ExtensionConfigManager::set_enabled(
set_extension_enabled(
&name_to_key(name),
selected.iter().any(|s| s.as_str() == name),
)?;
);
}
cliclack::outro("Extension settings updated successfully")?;
@@ -768,7 +768,7 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
.map(|(_, name, desc)| (name.to_string(), desc.to_string()))
.unwrap_or_else(|| (extension.clone(), extension.clone()));
ExtensionConfigManager::set(ExtensionEntry {
set_extension(ExtensionEntry {
enabled: true,
config: ExtensionConfig::Builtin {
name: extension.clone(),
@@ -778,12 +778,12 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
description,
available_tools: Vec::new(),
},
})?;
});
cliclack::outro(format!("Enabled {} extension", style(extension).green()))?;
}
"stdio" => {
let extensions = ExtensionConfigManager::get_all_names()?;
let extensions = get_all_extension_names();
let name: String = cliclack::input("What would you like to call this extension?")
.placeholder("my-extension")
.validate(move |input: &String| {
@@ -866,7 +866,7 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
}
}
ExtensionConfigManager::set(ExtensionEntry {
set_extension(ExtensionEntry {
enabled: true,
config: ExtensionConfig::Stdio {
name: name.clone(),
@@ -879,12 +879,12 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
bundled: None,
available_tools: Vec::new(),
},
})?;
});
cliclack::outro(format!("Added {} extension", style(name).green()))?;
}
"sse" => {
let extensions = ExtensionConfigManager::get_all_names()?;
let extensions = get_all_extension_names();
let name: String = cliclack::input("What would you like to call this extension?")
.placeholder("my-remote-extension")
.validate(move |input: &String| {
@@ -962,7 +962,7 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
}
}
ExtensionConfigManager::set(ExtensionEntry {
set_extension(ExtensionEntry {
enabled: true,
config: ExtensionConfig::Sse {
name: name.clone(),
@@ -974,12 +974,12 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
bundled: None,
available_tools: Vec::new(),
},
})?;
});
cliclack::outro(format!("Added {} extension", style(name).green()))?;
}
"streamable_http" => {
let extensions = ExtensionConfigManager::get_all_names()?;
let extensions = get_all_extension_names();
let name: String = cliclack::input("What would you like to call this extension?")
.placeholder("my-remote-extension")
.validate(move |input: &String| {
@@ -1082,7 +1082,7 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
}
}
ExtensionConfigManager::set(ExtensionEntry {
set_extension(ExtensionEntry {
enabled: true,
config: ExtensionConfig::StreamableHttp {
name: name.clone(),
@@ -1095,7 +1095,7 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
bundled: None,
available_tools: Vec::new(),
},
})?;
});
cliclack::outro(format!("Added {} extension", style(name).green()))?;
}
@@ -1106,7 +1106,7 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
}
pub fn remove_extension_dialog() -> Result<(), Box<dyn Error>> {
let extensions = ExtensionConfigManager::get_all()?;
let extensions = get_all_extensions();
// Create a list of extension names and their enabled status
let mut extension_status: Vec<(String, bool)> = extensions
@@ -1151,7 +1151,7 @@ pub fn remove_extension_dialog() -> Result<(), Box<dyn Error>> {
.interact()?;
for name in selected {
ExtensionConfigManager::remove(&name_to_key(name))?;
remove_extension(&name_to_key(name));
let mut permission_manager = PermissionManager::default();
permission_manager.remove_extension(&name_to_key(name));
cliclack::outro(format!("Removed {} extension", style(name).green()))?;
@@ -1386,11 +1386,9 @@ pub fn toggle_experiments_dialog() -> Result<(), Box<dyn Error>> {
}
pub async fn configure_tool_permissions_dialog() -> Result<(), Box<dyn Error>> {
let mut extensions: Vec<String> = ExtensionConfigManager::get_all()
.unwrap_or_default()
let mut extensions: Vec<String> = get_enabled_extensions()
.into_iter()
.filter(|ext| ext.enabled)
.map(|ext| ext.config.name().clone())
.map(|ext| ext.name().clone())
.collect();
extensions.push("platform".to_string());
@@ -1423,7 +1421,7 @@ pub async fn configure_tool_permissions_dialog() -> Result<(), Box<dyn Error>> {
let agent = Agent::new();
let new_provider = create(&provider_name, model_config)?;
agent.update_provider(new_provider).await?;
if let Ok(Some(config)) = ExtensionConfigManager::get_config_by_name(&selected_extension_name) {
if let Some(config) = get_extension_by_name(&selected_extension_name) {
agent
.add_extension(config.clone())
.await
@@ -1706,13 +1704,13 @@ pub async fn handle_openrouter_auth() -> Result<(), Box<dyn Error>> {
println!("✓ Configuration test passed!");
// Enable the developer extension by default if not already enabled
let entries = ExtensionConfigManager::get_all()?;
let entries = get_all_extensions();
let has_developer = entries
.iter()
.any(|e| e.config.name() == "developer" && e.enabled);
if !has_developer {
match ExtensionConfigManager::set(ExtensionEntry {
set_extension(ExtensionEntry {
enabled: true,
config: ExtensionConfig::Builtin {
name: "developer".to_string(),
@@ -1724,12 +1722,8 @@ pub async fn handle_openrouter_auth() -> Result<(), Box<dyn Error>> {
description: "Developer extension".to_string(),
available_tools: Vec::new(),
},
}) {
Ok(_) => println!("✓ Developer extension enabled"),
Err(e) => {
eprintln!("⚠️ Failed to enable developer extension: {}", e)
}
}
});
println!("✓ Developer extension enabled");
}
cliclack::outro("OpenRouter setup complete! You can now use goose.")?;
@@ -1809,13 +1803,13 @@ pub async fn handle_tetrate_auth() -> Result<(), Box<dyn Error>> {
println!("✓ Configuration test passed!");
// Enable the developer extension by default if not already enabled
let entries = ExtensionConfigManager::get_all()?;
let entries = get_all_extensions();
let has_developer = entries
.iter()
.any(|e| e.config.name() == "developer" && e.enabled);
if !has_developer {
match ExtensionConfigManager::set(ExtensionEntry {
set_extension(ExtensionEntry {
enabled: true,
config: ExtensionConfig::Builtin {
name: "developer".to_string(),
@@ -1827,12 +1821,8 @@ pub async fn handle_tetrate_auth() -> Result<(), Box<dyn Error>> {
description: "Developer extension".to_string(),
available_tools: Vec::new(),
},
}) {
Ok(_) => println!("✓ Developer extension enabled"),
Err(e) => {
eprintln!("⚠️ Failed to enable developer extension: {}", e)
}
}
});
println!("✓ Developer extension enabled");
}
cliclack::outro("Tetrate Agent Router Service setup complete! You can now use goose.")?;
+4 -10
View File
@@ -164,16 +164,10 @@ pub async fn handle_web(
agent.update_provider(provider).await?;
// Load and enable extensions from config
let extensions = goose::config::ExtensionConfigManager::get_all()?;
for ext_config in extensions {
if ext_config.enabled {
if let Err(e) = agent.add_extension(ext_config.config.clone()).await {
eprintln!(
"Warning: Failed to load extension {}: {}",
ext_config.config.name(),
e
);
}
let enabled_configs = goose::config::get_enabled_extensions();
for config in enabled_configs {
if let Err(e) = agent.add_extension(config.clone()).await {
eprintln!("Warning: Failed to load extension {}: {}", config.name(), e);
}
}
+94 -52
View File
@@ -1,14 +1,18 @@
use super::output;
use super::CliSession;
use console::style;
use goose::agents::types::RetryConfig;
use goose::agents::types::{RetryConfig, SessionConfig};
use goose::agents::Agent;
use goose::config::{Config, ExtensionConfig, ExtensionConfigManager};
use goose::config::{
extensions::{get_extension_by_name, set_extension, ExtensionEntry},
get_all_extensions, get_enabled_extensions, Config, ExtensionConfig,
};
use goose::providers::create;
use goose::recipe::{Response, SubRecipe};
use goose::agents::extension::PlatformExtensionContext;
use goose::session::SessionManager;
use goose::session::{EnabledExtensionsState, ExtensionState};
use rustyline::EditMode;
use std::collections::HashSet;
use std::process;
@@ -114,18 +118,17 @@ async fn offer_extension_debugging_help(
debug_agent.update_provider(provider).await?;
// Add the developer extension if available to help with debugging
if let Ok(extensions) = ExtensionConfigManager::get_all() {
for ext_wrapper in extensions {
if ext_wrapper.enabled && ext_wrapper.config.name() == "developer" {
if let Err(e) = debug_agent.add_extension(ext_wrapper.config).await {
// If we can't add developer extension, continue without it
eprintln!(
"Note: Could not load developer extension for debugging: {}",
e
);
}
break;
let extensions = get_all_extensions();
for ext_wrapper in extensions {
if ext_wrapper.enabled && ext_wrapper.config.name() == "developer" {
if let Err(e) = debug_agent.add_extension(ext_wrapper.config).await {
// If we can't add developer extension, continue without it
eprintln!(
"Note: Could not load developer extension for debugging: {}",
e
);
}
break;
}
}
@@ -151,6 +154,41 @@ async fn offer_extension_debugging_help(
Ok(())
}
fn check_missing_extensions_or_exit(saved_extensions: &[ExtensionConfig]) {
let missing: Vec<_> = saved_extensions
.iter()
.filter(|ext| get_extension_by_name(&ext.name()).is_none())
.cloned()
.collect();
if !missing.is_empty() {
let names = missing
.iter()
.map(|e| e.name())
.collect::<Vec<_>>()
.join(", ");
if !cliclack::confirm(format!(
"Extension(s) {} from previous session are no longer in config. Re-add them to config?",
names
))
.initial_value(true)
.interact()
.unwrap_or(false)
{
println!("{}", style("Resume cancelled.").yellow());
process::exit(0);
}
missing.into_iter().for_each(|config| {
set_extension(ExtensionEntry {
enabled: true,
config,
});
});
}
}
#[derive(Clone, Debug, Default)]
pub struct SessionSettings {
pub goose_model: Option<String>,
@@ -330,13 +368,26 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
let extensions_to_run: Vec<_> = if let Some(extensions) = session_config.extensions_override {
agent.disable_router_for_recipe().await;
extensions.into_iter().collect()
} else if session_config.resume {
if let Some(session_id) = session_id.as_ref() {
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);
saved_state.extensions
} else {
get_enabled_extensions()
}
}
_ => get_enabled_extensions(),
}
} else {
get_enabled_extensions()
}
} else {
ExtensionConfigManager::get_all()
.expect("should load extensions")
.into_iter()
.filter(|ext| ext.enabled)
.map(|ext| ext.config)
.collect()
get_enabled_extensions()
};
let mut set = JoinSet::new();
@@ -416,21 +467,17 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
session_config.retry_config.clone(),
);
// Add extensions if provided
// 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 extension '{}': {}",
"Warning: Failed to start stdio extension '{}' ({}), continuing without it",
extension_str, e
))
.yellow()
);
eprintln!(
"{}",
style(format!("Continuing without extension '{}'", extension_str)).yellow()
);
// Offer debugging help
if let Err(debug_err) = offer_extension_debugging_help(
@@ -452,19 +499,11 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
eprintln!(
"{}",
style(format!(
"Warning: Failed to start remote extension '{}': {}",
"Warning: Failed to start remote extension '{}' ({}), continuing without it",
extension_str, e
))
.yellow()
);
eprintln!(
"{}",
style(format!(
"Continuing without remote extension '{}'",
extension_str
))
.yellow()
);
// Offer debugging help
if let Err(debug_err) = offer_extension_debugging_help(
@@ -489,19 +528,11 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
eprintln!(
"{}",
style(format!(
"Warning: Failed to start streamable HTTP extension '{}': {}",
"Warning: Failed to start streamable HTTP extension '{}' ({}), continuing without it",
extension_str, e
))
.yellow()
);
eprintln!(
"{}",
style(format!(
"Continuing without streamable HTTP extension '{}'",
extension_str
))
.yellow()
);
// Offer debugging help
if let Err(debug_err) = offer_extension_debugging_help(
@@ -523,19 +554,11 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
eprintln!(
"{}",
style(format!(
"Warning: Failed to start builtin extension '{}': {}",
"Warning: Failed to start builtin extension '{}' ({}), continuing without it",
builtin, e
))
.yellow()
);
eprintln!(
"{}",
style(format!(
"Continuing without builtin extension '{}'",
builtin
))
.yellow()
);
// Offer debugging help
if let Err(debug_err) = offer_extension_debugging_help(
@@ -551,6 +574,25 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
}
}
if let Some(session_id) = session_id.as_ref() {
let session_config_for_save = SessionConfig {
id: session_id.clone(),
working_dir: std::env::current_dir().unwrap_or_default(),
schedule_id: None,
execution_mode: None,
max_turns: None,
retry_config: None,
};
if let Err(e) = session
.agent
.save_extension_state(&session_config_for_save)
.await
{
tracing::warn!("Failed to save initial extension state: {}", e);
}
}
// Add CLI-specific system prompt extension
session
.agent
+4 -4
View File
@@ -31,14 +31,13 @@ use goose::agents::types::RetryConfig;
use goose::agents::{Agent, SessionConfig};
use goose::config::Config;
use goose::providers::pricing::initialize_pricing_cache;
use goose::session;
use goose::session::SessionManager;
use input::InputResult;
use rmcp::model::PromptMessage;
use rmcp::model::ServerNotification;
use rmcp::model::{ErrorCode, ErrorData};
use goose::conversation::message::{Message, MessageContent};
use goose::session::SessionManager;
use rand::{distributions::Alphanumeric, Rng};
use rustyline::EditMode;
use serde_json::Value;
@@ -300,8 +299,9 @@ impl CliSession {
/// * `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().to_string();
let config = ExtensionConfig::Builtin {
name: name.trim().to_string(),
name: extension_name,
display_name: None,
// TODO: should set a timeout
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
@@ -1464,7 +1464,7 @@ impl CliSession {
);
}
pub async fn get_metadata(&self) -> Result<session::Session> {
pub async fn get_metadata(&self) -> Result<goose::session::Session> {
match &self.session_id {
Some(id) => SessionManager::get_session(id, false).await,
None => Err(anyhow::anyhow!("No session available")),