resolved all the extensions to load in cli (#6464)
This commit is contained in:
+11
-42
@@ -1,8 +1,9 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::{Args, CommandFactory, Parser, Subcommand};
|
use clap::{Args, CommandFactory, Parser, Subcommand};
|
||||||
use clap_complete::{generate, Shell as ClapShell};
|
use clap_complete::{generate, Shell as ClapShell};
|
||||||
use goose::config::{Config, ExtensionConfig};
|
use goose::config::Config;
|
||||||
use goose::posthog::get_telemetry_choice;
|
use goose::posthog::get_telemetry_choice;
|
||||||
|
use goose::recipe::Recipe;
|
||||||
use goose_mcp::mcp_server_runner::{serve, McpCommand};
|
use goose_mcp::mcp_server_runner::{serve, McpCommand};
|
||||||
use goose_mcp::{
|
use goose_mcp::{
|
||||||
AutoVisualiserRouter, ComputerControllerServer, DeveloperServer, MemoryServer, TutorialServer,
|
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::commands::session::{handle_session_list, handle_session_remove};
|
||||||
use crate::recipes::extract_from_cli::extract_recipe_info_from_cli;
|
use crate::recipes::extract_from_cli::extract_recipe_info_from_cli;
|
||||||
use crate::recipes::recipe::{explain_recipe, render_recipe_as_yaml};
|
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::session_manager::SessionType;
|
||||||
use goose::session::SessionManager;
|
use goose::session::SessionManager;
|
||||||
use goose_bench::bench_config::BenchRunConfig;
|
use goose_bench::bench_config::BenchRunConfig;
|
||||||
@@ -941,18 +942,9 @@ enum CliProviderVariant {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct InputConfig {
|
pub struct InputConfig {
|
||||||
pub contents: Option<String>,
|
pub contents: Option<String>,
|
||||||
pub extensions_override: Option<Vec<ExtensionConfig>>,
|
|
||||||
pub additional_system_prompt: Option<String>,
|
pub additional_system_prompt: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct RecipeInfo {
|
|
||||||
pub session_settings: Option<SessionSettings>,
|
|
||||||
pub sub_recipes: Option<Vec<goose::recipe::SubRecipe>>,
|
|
||||||
pub final_output_response: Option<goose::recipe::Response>,
|
|
||||||
pub retry_config: Option<goose::agents::types::RetryConfig>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_command_name(command: &Option<Command>) -> &'static str {
|
fn get_command_name(command: &Option<Command>) -> &'static str {
|
||||||
match command {
|
match command {
|
||||||
Some(Command::Configure {}) => "configure",
|
Some(Command::Configure {}) => "configure",
|
||||||
@@ -1093,9 +1085,8 @@ async fn handle_interactive_session(
|
|||||||
extensions: extension_opts.extensions,
|
extensions: extension_opts.extensions,
|
||||||
streamable_http_extensions: extension_opts.streamable_http_extensions,
|
streamable_http_extensions: extension_opts.streamable_http_extensions,
|
||||||
builtins: extension_opts.builtins,
|
builtins: extension_opts.builtins,
|
||||||
extensions_override: None,
|
recipe: None,
|
||||||
additional_system_prompt: None,
|
additional_system_prompt: None,
|
||||||
settings: None,
|
|
||||||
provider: None,
|
provider: None,
|
||||||
model: None,
|
model: None,
|
||||||
debug: session_opts.debug,
|
debug: session_opts.debug,
|
||||||
@@ -1104,9 +1095,6 @@ async fn handle_interactive_session(
|
|||||||
scheduled_job_id: None,
|
scheduled_job_id: None,
|
||||||
interactive: true,
|
interactive: true,
|
||||||
quiet: false,
|
quiet: false,
|
||||||
sub_recipes: None,
|
|
||||||
final_output_response: None,
|
|
||||||
retry_config: None,
|
|
||||||
output_format: "text".to_string(),
|
output_format: "text".to_string(),
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
@@ -1163,7 +1151,7 @@ async fn log_session_completion(
|
|||||||
fn parse_run_input(
|
fn parse_run_input(
|
||||||
input_opts: &InputOptions,
|
input_opts: &InputOptions,
|
||||||
quiet: bool,
|
quiet: bool,
|
||||||
) -> Result<Option<(InputConfig, Option<RecipeInfo>)>> {
|
) -> Result<Option<(InputConfig, Option<Recipe>)>> {
|
||||||
match (
|
match (
|
||||||
&input_opts.instructions,
|
&input_opts.instructions,
|
||||||
&input_opts.input_text,
|
&input_opts.input_text,
|
||||||
@@ -1177,7 +1165,6 @@ fn parse_run_input(
|
|||||||
Ok(Some((
|
Ok(Some((
|
||||||
InputConfig {
|
InputConfig {
|
||||||
contents: Some(contents),
|
contents: Some(contents),
|
||||||
extensions_override: None,
|
|
||||||
additional_system_prompt: input_opts.system.clone(),
|
additional_system_prompt: input_opts.system.clone(),
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
@@ -1194,7 +1181,6 @@ fn parse_run_input(
|
|||||||
Ok(Some((
|
Ok(Some((
|
||||||
InputConfig {
|
InputConfig {
|
||||||
contents: Some(contents),
|
contents: Some(contents),
|
||||||
extensions_override: None,
|
|
||||||
additional_system_prompt: None,
|
additional_system_prompt: None,
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
@@ -1203,7 +1189,6 @@ fn parse_run_input(
|
|||||||
(_, Some(text), _) => Ok(Some((
|
(_, Some(text), _) => Ok(Some((
|
||||||
InputConfig {
|
InputConfig {
|
||||||
contents: Some(text.clone()),
|
contents: Some(text.clone()),
|
||||||
extensions_override: None,
|
|
||||||
additional_system_prompt: input_opts.system.clone(),
|
additional_system_prompt: input_opts.system.clone(),
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
@@ -1247,13 +1232,13 @@ fn parse_run_input(
|
|||||||
"Recipe execution started"
|
"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(),
|
recipe_name.clone(),
|
||||||
input_opts.params.clone(),
|
input_opts.params.clone(),
|
||||||
input_opts.additional_sub_recipes.clone(),
|
input_opts.additional_sub_recipes.clone(),
|
||||||
quiet,
|
quiet,
|
||||||
)?;
|
)?;
|
||||||
Ok(Some((input_config, Some(recipe_info))))
|
Ok(Some((input_config, Some(recipe))))
|
||||||
}
|
}
|
||||||
(None, None, None) => {
|
(None, None, None) => {
|
||||||
eprintln!("Error: Must provide either --instructions (-i), --text (-t), or --recipe. Use -i - for stdin.");
|
eprintln!("Error: Must provide either --instructions (-i), --text (-t), or --recipe. Use -i - for stdin.");
|
||||||
@@ -1277,7 +1262,7 @@ async fn handle_run_command(
|
|||||||
|
|
||||||
let parsed = parse_run_input(&input_opts, output_opts.quiet)?;
|
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(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1302,11 +1287,8 @@ async fn handle_run_command(
|
|||||||
extensions: extension_opts.extensions,
|
extensions: extension_opts.extensions,
|
||||||
streamable_http_extensions: extension_opts.streamable_http_extensions,
|
streamable_http_extensions: extension_opts.streamable_http_extensions,
|
||||||
builtins: extension_opts.builtins,
|
builtins: extension_opts.builtins,
|
||||||
extensions_override: input_config.extensions_override,
|
recipe: recipe.clone(),
|
||||||
additional_system_prompt: input_config.additional_system_prompt,
|
additional_system_prompt: input_config.additional_system_prompt,
|
||||||
settings: recipe_info
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|r| r.session_settings.clone()),
|
|
||||||
provider: model_opts.provider,
|
provider: model_opts.provider,
|
||||||
model: model_opts.model,
|
model: model_opts.model,
|
||||||
debug: session_opts.debug,
|
debug: session_opts.debug,
|
||||||
@@ -1315,11 +1297,6 @@ async fn handle_run_command(
|
|||||||
scheduled_job_id: run_behavior.scheduled_job_id,
|
scheduled_job_id: run_behavior.scheduled_job_id,
|
||||||
interactive: run_behavior.interactive,
|
interactive: run_behavior.interactive,
|
||||||
quiet: output_opts.quiet,
|
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,
|
output_format: output_opts.output_format,
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
@@ -1328,11 +1305,7 @@ async fn handle_run_command(
|
|||||||
session.interactive(input_config.contents).await
|
session.interactive(input_config.contents).await
|
||||||
} else if let Some(contents) = input_config.contents {
|
} else if let Some(contents) = input_config.contents {
|
||||||
let session_start = std::time::Instant::now();
|
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" };
|
||||||
"recipe"
|
|
||||||
} else {
|
|
||||||
"run"
|
|
||||||
};
|
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
counter.goose.session_starts = 1,
|
counter.goose.session_starts = 1,
|
||||||
@@ -1438,9 +1411,8 @@ async fn handle_default_session() -> Result<()> {
|
|||||||
extensions: Vec::new(),
|
extensions: Vec::new(),
|
||||||
streamable_http_extensions: Vec::new(),
|
streamable_http_extensions: Vec::new(),
|
||||||
builtins: Vec::new(),
|
builtins: Vec::new(),
|
||||||
extensions_override: None,
|
recipe: None,
|
||||||
additional_system_prompt: None,
|
additional_system_prompt: None,
|
||||||
settings: None::<SessionSettings>,
|
|
||||||
provider: None,
|
provider: None,
|
||||||
model: None,
|
model: None,
|
||||||
debug: false,
|
debug: false,
|
||||||
@@ -1449,9 +1421,6 @@ async fn handle_default_session() -> Result<()> {
|
|||||||
scheduled_job_id: None,
|
scheduled_job_id: None,
|
||||||
interactive: true,
|
interactive: true,
|
||||||
quiet: false,
|
quiet: false,
|
||||||
sub_recipes: None,
|
|
||||||
final_output_response: None,
|
|
||||||
retry_config: None,
|
|
||||||
output_format: "text".to_string(),
|
output_format: "text".to_string(),
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|||||||
@@ -41,9 +41,8 @@ pub async fn agent_generator(
|
|||||||
extensions: requirements.external,
|
extensions: requirements.external,
|
||||||
streamable_http_extensions: requirements.streamable_http,
|
streamable_http_extensions: requirements.streamable_http,
|
||||||
builtins: requirements.builtin,
|
builtins: requirements.builtin,
|
||||||
extensions_override: None,
|
recipe: None,
|
||||||
additional_system_prompt: None,
|
additional_system_prompt: None,
|
||||||
settings: None,
|
|
||||||
provider: None,
|
provider: None,
|
||||||
model: None,
|
model: None,
|
||||||
debug: false,
|
debug: false,
|
||||||
@@ -52,9 +51,6 @@ pub async fn agent_generator(
|
|||||||
scheduled_job_id: None,
|
scheduled_job_id: None,
|
||||||
max_turns: None,
|
max_turns: None,
|
||||||
quiet: false,
|
quiet: false,
|
||||||
sub_recipes: None,
|
|
||||||
final_output_response: None,
|
|
||||||
retry_config: None,
|
|
||||||
output_format: "text".to_string(),
|
output_format: "text".to_string(),
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|||||||
@@ -1,31 +1,29 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
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::print_recipe::print_recipe_info;
|
||||||
use crate::recipes::recipe::load_recipe;
|
use crate::recipes::recipe::load_recipe;
|
||||||
use crate::recipes::search_recipe::load_recipe_file;
|
use crate::recipes::search_recipe::load_recipe_file;
|
||||||
use crate::{
|
|
||||||
cli::{InputConfig, RecipeInfo},
|
|
||||||
session::SessionSettings,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn extract_recipe_info_from_cli(
|
pub fn extract_recipe_info_from_cli(
|
||||||
recipe_name: String,
|
recipe_name: String,
|
||||||
params: Vec<(String, String)>,
|
params: Vec<(String, String)>,
|
||||||
additional_sub_recipes: Vec<String>,
|
additional_sub_recipes: Vec<String>,
|
||||||
quiet: bool,
|
quiet: bool,
|
||||||
) -> Result<(InputConfig, RecipeInfo)> {
|
) -> Result<(InputConfig, Recipe)> {
|
||||||
let recipe = load_recipe(&recipe_name, params.clone()).unwrap_or_else(|err| {
|
let mut recipe = load_recipe(&recipe_name, params.clone()).unwrap_or_else(|err| {
|
||||||
eprintln!("{}: {}", console::style("Error").red().bold(), err);
|
eprintln!("{}: {}", console::style("Error").red().bold(), err);
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
});
|
});
|
||||||
if !quiet {
|
if !quiet {
|
||||||
print_recipe_info(&recipe, params);
|
print_recipe_info(&recipe, params);
|
||||||
}
|
}
|
||||||
let mut all_sub_recipes = recipe.sub_recipes.clone().unwrap_or_default();
|
|
||||||
if !additional_sub_recipes.is_empty() {
|
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 {
|
for sub_recipe_name in additional_sub_recipes {
|
||||||
match load_recipe_file(&sub_recipe_name) {
|
match load_recipe_file(&sub_recipe_name) {
|
||||||
Ok(recipe_file) => {
|
Ok(recipe_file) => {
|
||||||
@@ -49,25 +47,15 @@ pub fn extract_recipe_info_from_cli(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
recipe.sub_recipes = Some(all_sub_recipes);
|
||||||
}
|
}
|
||||||
|
|
||||||
let input_config = InputConfig {
|
let input_config = InputConfig {
|
||||||
contents: recipe.prompt.filter(|s| !s.trim().is_empty()),
|
contents: recipe.prompt.clone().filter(|s| !s.trim().is_empty()),
|
||||||
extensions_override: recipe.extensions,
|
additional_system_prompt: recipe.instructions.clone(),
|
||||||
additional_system_prompt: recipe.instructions,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let recipe_info = RecipeInfo {
|
Ok((input_config, recipe))
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok((input_config, recipe_info))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_recipe_name(recipe_identifier: &str) -> String {
|
fn extract_recipe_name(recipe_identifier: &str) -> String {
|
||||||
@@ -98,18 +86,18 @@ mod tests {
|
|||||||
let params = vec![("name".to_string(), "my_value".to_string())];
|
let params = vec![("name".to_string(), "my_value".to_string())];
|
||||||
let recipe_name = recipe_path.to_str().unwrap().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();
|
extract_recipe_info_from_cli(recipe_name, params, Vec::new(), false).unwrap();
|
||||||
let settings = recipe_info.session_settings;
|
let settings = recipe.settings;
|
||||||
let sub_recipes = recipe_info.sub_recipes;
|
let sub_recipes = recipe.sub_recipes;
|
||||||
let response = recipe_info.final_output_response;
|
let response = recipe.response;
|
||||||
|
|
||||||
assert_eq!(input_config.contents, Some("test_prompt".to_string()));
|
assert_eq!(input_config.contents, Some("test_prompt".to_string()));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
input_config.additional_system_prompt,
|
input_config.additional_system_prompt,
|
||||||
Some("test_instructions my_value".to_string())
|
Some("test_instructions my_value".to_string())
|
||||||
);
|
);
|
||||||
assert!(input_config.extensions_override.is_none());
|
assert!(recipe.extensions.is_none());
|
||||||
|
|
||||||
assert!(settings.is_some());
|
assert!(settings.is_some());
|
||||||
let settings = settings.unwrap();
|
let settings = settings.unwrap();
|
||||||
@@ -162,19 +150,19 @@ mod tests {
|
|||||||
sub_recipe2_path.to_string_lossy().to_string(),
|
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)
|
extract_recipe_info_from_cli(recipe_name, params, additional_sub_recipes, false)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let settings = recipe_info.session_settings;
|
let settings = recipe.settings;
|
||||||
let sub_recipes = recipe_info.sub_recipes;
|
let sub_recipes = recipe.sub_recipes;
|
||||||
let response = recipe_info.final_output_response;
|
let response = recipe.response;
|
||||||
|
|
||||||
assert_eq!(input_config.contents, Some("test_prompt".to_string()));
|
assert_eq!(input_config.contents, Some("test_prompt".to_string()));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
input_config.additional_system_prompt,
|
input_config.additional_system_prompt,
|
||||||
Some("test_instructions my_value".to_string())
|
Some("test_instructions my_value".to_string())
|
||||||
);
|
);
|
||||||
assert!(input_config.extensions_override.is_none());
|
assert!(recipe.extensions.is_none());
|
||||||
|
|
||||||
assert!(settings.is_some());
|
assert!(settings.is_some());
|
||||||
let settings = settings.unwrap();
|
let settings = settings.unwrap();
|
||||||
|
|||||||
@@ -1,23 +1,77 @@
|
|||||||
use super::output;
|
use super::output;
|
||||||
use super::CliSession;
|
use super::CliSession;
|
||||||
use console::style;
|
use console::style;
|
||||||
use goose::agents::types::{RetryConfig, SessionConfig};
|
|
||||||
use goose::agents::Agent;
|
use goose::agents::Agent;
|
||||||
|
use goose::config::get_enabled_extensions;
|
||||||
|
use goose::config::resolve_extensions_for_new_session;
|
||||||
use goose::config::{
|
use goose::config::{
|
||||||
extensions::get_extension_by_name, get_all_extensions, get_enabled_extensions, Config,
|
extensions::get_extension_by_name, get_all_extensions, Config, ExtensionConfig,
|
||||||
ExtensionConfig,
|
|
||||||
};
|
};
|
||||||
use goose::providers::create;
|
use goose::providers::create;
|
||||||
use goose::recipe::{Response, SubRecipe};
|
use goose::recipe::Recipe;
|
||||||
|
|
||||||
use goose::session::session_manager::SessionType;
|
use goose::session::session_manager::SessionType;
|
||||||
use goose::session::{EnabledExtensionsState, ExtensionState};
|
use goose::session::{EnabledExtensionsState, ExtensionState};
|
||||||
use rustyline::EditMode;
|
use rustyline::EditMode;
|
||||||
use std::collections::HashSet;
|
use std::collections::BTreeSet;
|
||||||
use std::process;
|
use std::process;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::task::JoinSet;
|
use tokio::task::JoinSet;
|
||||||
|
|
||||||
|
const EXTENSION_HINT_MAX_LEN: usize = 5;
|
||||||
|
|
||||||
|
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
|
/// Configuration for building a new Goose session
|
||||||
///
|
///
|
||||||
/// This struct contains all the parameters needed to create a new session,
|
/// This struct contains all the parameters needed to create a new session,
|
||||||
@@ -36,12 +90,10 @@ pub struct SessionBuilderConfig {
|
|||||||
pub streamable_http_extensions: Vec<String>,
|
pub streamable_http_extensions: Vec<String>,
|
||||||
/// List of builtin extension commands to add
|
/// List of builtin extension commands to add
|
||||||
pub builtins: Vec<String>,
|
pub builtins: Vec<String>,
|
||||||
/// List of extensions to enable, enable only this set and ignore configured ones
|
/// Recipe for the session
|
||||||
pub extensions_override: Option<Vec<ExtensionConfig>>,
|
pub recipe: Option<Recipe>,
|
||||||
/// Any additional system prompt to append to the default
|
/// Any additional system prompt to append to the default
|
||||||
pub additional_system_prompt: Option<String>,
|
pub additional_system_prompt: Option<String>,
|
||||||
/// Settings to override the global Goose settings
|
|
||||||
pub settings: Option<SessionSettings>,
|
|
||||||
/// Provider override from CLI arguments
|
/// Provider override from CLI arguments
|
||||||
pub provider: Option<String>,
|
pub provider: Option<String>,
|
||||||
/// Model override from CLI arguments
|
/// Model override from CLI arguments
|
||||||
@@ -58,12 +110,6 @@ pub struct SessionBuilderConfig {
|
|||||||
pub interactive: bool,
|
pub interactive: bool,
|
||||||
/// Quiet mode - suppress non-response output
|
/// Quiet mode - suppress non-response output
|
||||||
pub quiet: bool,
|
pub quiet: bool,
|
||||||
/// Sub-recipes to add to the session
|
|
||||||
pub sub_recipes: Option<Vec<SubRecipe>>,
|
|
||||||
/// Final output expected response
|
|
||||||
pub final_output_response: Option<Response>,
|
|
||||||
/// Retry configuration for automated validation and recovery
|
|
||||||
pub retry_config: Option<RetryConfig>,
|
|
||||||
/// Output format (text, json)
|
/// Output format (text, json)
|
||||||
pub output_format: String,
|
pub output_format: String,
|
||||||
}
|
}
|
||||||
@@ -79,9 +125,8 @@ impl Default for SessionBuilderConfig {
|
|||||||
extensions: Vec::new(),
|
extensions: Vec::new(),
|
||||||
streamable_http_extensions: Vec::new(),
|
streamable_http_extensions: Vec::new(),
|
||||||
builtins: Vec::new(),
|
builtins: Vec::new(),
|
||||||
extensions_override: None,
|
recipe: None,
|
||||||
additional_system_prompt: None,
|
additional_system_prompt: None,
|
||||||
settings: None,
|
|
||||||
provider: None,
|
provider: None,
|
||||||
model: None,
|
model: None,
|
||||||
debug: false,
|
debug: false,
|
||||||
@@ -90,9 +135,6 @@ impl Default for SessionBuilderConfig {
|
|||||||
scheduled_job_id: None,
|
scheduled_job_id: None,
|
||||||
interactive: false,
|
interactive: false,
|
||||||
quiet: false,
|
quiet: false,
|
||||||
sub_recipes: None,
|
|
||||||
final_output_response: None,
|
|
||||||
retry_config: None,
|
|
||||||
output_format: "text".to_string(),
|
output_format: "text".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,6 +245,85 @@ async fn offer_extension_debugging_help(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn load_extensions(
|
||||||
|
agent: Agent,
|
||||||
|
extensions_to_load: Vec<(String, ExtensionConfig)>,
|
||||||
|
provider_for_debug: Arc<dyn goose::providers::base::Provider>,
|
||||||
|
interactive: bool,
|
||||||
|
) -> Arc<Agent> {
|
||||||
|
let mut set = JoinSet::new();
|
||||||
|
let agent_ptr = Arc::new(agent);
|
||||||
|
|
||||||
|
let mut waiting_ids: BTreeSet<usize> = (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<usize>| {
|
||||||
|
let labels: Vec<String> = 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) {
|
fn check_missing_extensions_or_exit(saved_extensions: &[ExtensionConfig], interactive: bool) {
|
||||||
let missing: Vec<_> = saved_extensions
|
let missing: Vec<_> = saved_extensions
|
||||||
.iter()
|
.iter()
|
||||||
@@ -242,13 +363,6 @@ fn check_missing_extensions_or_exit(saved_extensions: &[ExtensionConfig], intera
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
|
||||||
pub struct SessionSettings {
|
|
||||||
pub goose_model: Option<String>,
|
|
||||||
pub goose_provider: Option<String>,
|
|
||||||
pub temperature: Option<f32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
|
pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
|
||||||
goose::posthog::set_session_context("cli", session_config.resume);
|
goose::posthog::set_session_context("cli", session_config.resume);
|
||||||
|
|
||||||
@@ -269,27 +383,20 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
|
|||||||
(None, None)
|
(None, None)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let recipe = session_config.recipe.as_ref();
|
||||||
|
let recipe_settings = recipe.and_then(|r| r.settings.as_ref());
|
||||||
|
|
||||||
let provider_name = session_config
|
let provider_name = session_config
|
||||||
.provider
|
.provider
|
||||||
.or(saved_provider)
|
.or(saved_provider)
|
||||||
.or_else(|| {
|
.or_else(|| recipe_settings.and_then(|s| s.goose_provider.clone()))
|
||||||
session_config
|
|
||||||
.settings
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|s| s.goose_provider.clone())
|
|
||||||
})
|
|
||||||
.or_else(|| config.get_goose_provider().ok())
|
.or_else(|| config.get_goose_provider().ok())
|
||||||
.expect("No provider configured. Run 'goose configure' first");
|
.expect("No provider configured. Run 'goose configure' first");
|
||||||
|
|
||||||
let model_name = session_config
|
let model_name = session_config
|
||||||
.model
|
.model
|
||||||
.or_else(|| saved_model_config.as_ref().map(|mc| mc.model_name.clone()))
|
.or_else(|| saved_model_config.as_ref().map(|mc| mc.model_name.clone()))
|
||||||
.or_else(|| {
|
.or_else(|| recipe_settings.and_then(|s| s.goose_model.clone()))
|
||||||
session_config
|
|
||||||
.settings
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|s| s.goose_model.clone())
|
|
||||||
})
|
|
||||||
.or_else(|| config.get_goose_model().ok())
|
.or_else(|| config.get_goose_model().ok())
|
||||||
.expect("No model configured. Run 'goose configure' first");
|
.expect("No model configured. Run 'goose configure' first");
|
||||||
|
|
||||||
@@ -299,12 +406,12 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
|
|||||||
.is_some_and(|mc| mc.model_name == model_name)
|
.is_some_and(|mc| mc.model_name == model_name)
|
||||||
{
|
{
|
||||||
let mut config = saved_model_config.unwrap();
|
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 = config.with_temperature(Some(temp));
|
||||||
}
|
}
|
||||||
config
|
config
|
||||||
} else {
|
} 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)
|
goose::model::ModelConfig::new(&model_name)
|
||||||
.unwrap_or_else(|e| {
|
.unwrap_or_else(|e| {
|
||||||
output::render_error(&format!("Failed to create model configuration: {}", e));
|
output::render_error(&format!("Failed to create model configuration: {}", e));
|
||||||
@@ -315,8 +422,8 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
|
|||||||
|
|
||||||
agent
|
agent
|
||||||
.apply_recipe_components(
|
.apply_recipe_components(
|
||||||
session_config.sub_recipes,
|
recipe.and_then(|r| r.sub_recipes.clone()),
|
||||||
session_config.final_output_response,
|
recipe.and_then(|r| r.response.clone()),
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -440,94 +547,42 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
|
|||||||
eprintln!("{}", style(format!("Warning: {}", warning)).yellow());
|
eprintln!("{}", style(format!("Warning: {}", warning)).yellow());
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we get extensions_override, only run those extensions and none other
|
let configured_extensions: Vec<ExtensionConfig> = if session_config.resume {
|
||||||
let extensions_to_run: Vec<_> = if let Some(extensions) = session_config.extensions_override {
|
agent
|
||||||
extensions.into_iter().collect()
|
|
||||||
} else if session_config.resume {
|
|
||||||
match agent
|
|
||||||
.config
|
.config
|
||||||
.session_manager
|
.session_manager
|
||||||
.get_session(&session_id, false)
|
.get_session(&session_id, false)
|
||||||
.await
|
.await
|
||||||
{
|
.ok()
|
||||||
Ok(session_data) => {
|
.and_then(|s| EnabledExtensionsState::from_extension_data(&s.extension_data))
|
||||||
if let Some(saved_state) =
|
.map(|state| {
|
||||||
EnabledExtensionsState::from_extension_data(&session_data.extension_data)
|
check_missing_extensions_or_exit(&state.extensions, session_config.interactive);
|
||||||
{
|
state.extensions
|
||||||
check_missing_extensions_or_exit(
|
})
|
||||||
&saved_state.extensions,
|
.unwrap_or_else(get_enabled_extensions)
|
||||||
session_config.interactive,
|
|
||||||
);
|
|
||||||
saved_state.extensions
|
|
||||||
} else {
|
|
||||||
get_enabled_extensions()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => get_enabled_extensions(),
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
get_enabled_extensions()
|
resolve_extensions_for_new_session(recipe.and_then(|r| r.extensions.as_deref()), None)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut set = JoinSet::new();
|
let cli_flag_extensions_to_load = parse_cli_flag_extensions(
|
||||||
let agent_ptr = Arc::new(agent);
|
&session_config.extensions,
|
||||||
|
&session_config.streamable_http_extensions,
|
||||||
|
&session_config.builtins,
|
||||||
|
);
|
||||||
|
|
||||||
let mut waiting_on = HashSet::new();
|
let mut extensions_to_load: Vec<(String, ExtensionConfig)> = configured_extensions
|
||||||
for extension in extensions_to_run {
|
.iter()
|
||||||
waiting_on.insert(extension.name());
|
.map(|cfg| (cfg.name(), cfg.clone()))
|
||||||
let agent_ptr = agent_ptr.clone();
|
.collect();
|
||||||
set.spawn(async move {
|
extensions_to_load.extend(cli_flag_extensions_to_load);
|
||||||
(
|
|
||||||
extension.name(),
|
|
||||||
agent_ptr.add_extension(extension.clone()).await,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let get_message = |waiting_on: &HashSet<String>| {
|
let agent_ptr = load_extensions(
|
||||||
let mut names: Vec<_> = waiting_on.iter().cloned().collect();
|
agent,
|
||||||
names.sort();
|
extensions_to_load,
|
||||||
format!("starting {} extensions: {}", names.len(), names.join(", "))
|
Arc::clone(&provider_for_display),
|
||||||
};
|
session_config.interactive,
|
||||||
|
)
|
||||||
let spinner = cliclack::spinner();
|
.await;
|
||||||
spinner.start(get_message(&waiting_on));
|
|
||||||
|
|
||||||
let mut offer_debug = 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((name, Err(e))) => offer_debug.push((name, e)),
|
|
||||||
Err(e) => tracing::error!("failed to add extension: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
spinner.clear();
|
|
||||||
|
|
||||||
for (name, err) in offer_debug {
|
|
||||||
eprintln!(
|
|
||||||
"{}",
|
|
||||||
style(format!(
|
|
||||||
"Warning: Failed to start extension '{}' ({}), continuing without it",
|
|
||||||
name, err
|
|
||||||
))
|
|
||||||
.yellow()
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Err(debug_err) = offer_extension_debugging_help(
|
|
||||||
&name,
|
|
||||||
&err.to_string(),
|
|
||||||
Arc::clone(&provider_for_display),
|
|
||||||
session_config.interactive,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
eprintln!("Note: Could not start debugging session: {}", debug_err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine editor mode
|
// Determine editor mode
|
||||||
let edit_mode = config
|
let edit_mode = config
|
||||||
@@ -544,113 +599,24 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
|
|||||||
|
|
||||||
let debug_mode = session_config.debug || config.get_param("GOOSE_DEBUG").unwrap_or(false);
|
let debug_mode = session_config.debug || config.get_param("GOOSE_DEBUG").unwrap_or(false);
|
||||||
|
|
||||||
// Create new session
|
let session = CliSession::new(
|
||||||
let mut session = CliSession::new(
|
|
||||||
Arc::try_unwrap(agent_ptr).unwrap_or_else(|_| panic!("There should be no more references")),
|
Arc::try_unwrap(agent_ptr).unwrap_or_else(|_| panic!("There should be no more references")),
|
||||||
session_id.clone(),
|
session_id.clone(),
|
||||||
debug_mode,
|
debug_mode,
|
||||||
session_config.scheduled_job_id.clone(),
|
session_config.scheduled_job_id.clone(),
|
||||||
session_config.max_turns,
|
session_config.max_turns,
|
||||||
edit_mode,
|
edit_mode,
|
||||||
session_config.retry_config.clone(),
|
recipe.and_then(|r| r.retry.clone()),
|
||||||
session_config.output_format.clone(),
|
session_config.output_format.clone(),
|
||||||
)
|
)
|
||||||
.await;
|
.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
|
if let Err(e) = session
|
||||||
.agent
|
.agent
|
||||||
.save_extension_state(&session_config_for_save)
|
.persist_extension_state(&session_id.clone())
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
tracing::warn!("Failed to save initial extension state: {}", e);
|
tracing::warn!("Failed to save extension state: {}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add CLI-specific system prompt extension
|
// Add CLI-specific system prompt extension
|
||||||
@@ -697,9 +663,8 @@ mod tests {
|
|||||||
extensions: vec!["echo test".to_string()],
|
extensions: vec!["echo test".to_string()],
|
||||||
streamable_http_extensions: vec!["http://localhost:8080/mcp".to_string()],
|
streamable_http_extensions: vec!["http://localhost:8080/mcp".to_string()],
|
||||||
builtins: vec!["developer".to_string()],
|
builtins: vec!["developer".to_string()],
|
||||||
extensions_override: None,
|
recipe: None,
|
||||||
additional_system_prompt: Some("Test prompt".to_string()),
|
additional_system_prompt: Some("Test prompt".to_string()),
|
||||||
settings: None,
|
|
||||||
provider: None,
|
provider: None,
|
||||||
model: None,
|
model: None,
|
||||||
debug: true,
|
debug: true,
|
||||||
@@ -708,9 +673,6 @@ mod tests {
|
|||||||
scheduled_job_id: None,
|
scheduled_job_id: None,
|
||||||
interactive: true,
|
interactive: true,
|
||||||
quiet: false,
|
quiet: false,
|
||||||
sub_recipes: None,
|
|
||||||
final_output_response: None,
|
|
||||||
retry_config: None,
|
|
||||||
output_format: "text".to_string(),
|
output_format: "text".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -735,7 +697,7 @@ mod tests {
|
|||||||
assert!(config.extensions.is_empty());
|
assert!(config.extensions.is_empty());
|
||||||
assert!(config.streamable_http_extensions.is_empty());
|
assert!(config.streamable_http_extensions.is_empty());
|
||||||
assert!(config.builtins.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.additional_system_prompt.is_none());
|
||||||
assert!(!config.debug);
|
assert!(!config.debug);
|
||||||
assert!(config.max_tool_repetitions.is_none());
|
assert!(config.max_tool_repetitions.is_none());
|
||||||
@@ -743,7 +705,6 @@ mod tests {
|
|||||||
assert!(config.scheduled_job_id.is_none());
|
assert!(config.scheduled_job_id.is_none());
|
||||||
assert!(!config.interactive);
|
assert!(!config.interactive);
|
||||||
assert!(!config.quiet);
|
assert!(!config.quiet);
|
||||||
assert!(config.final_output_response.is_none());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -760,4 +721,16 @@ mod tests {
|
|||||||
assert_eq!(extension_name, "test-extension");
|
assert_eq!(extension_name, "test-extension");
|
||||||
assert_eq!(error_message, "test error");
|
assert_eq!(error_message, "test error");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_truncate_with_ellipsis() {
|
||||||
|
assert_eq!(truncate_with_ellipsis("abc", 5), "abc");
|
||||||
|
|
||||||
|
assert_eq!(truncate_with_ellipsis("abcde", 5), "abcde");
|
||||||
|
|
||||||
|
assert_eq!(truncate_with_ellipsis("abcdef", 5), "abcde…");
|
||||||
|
assert_eq!(truncate_with_ellipsis("hello world", 5), "hello…");
|
||||||
|
|
||||||
|
assert_eq!(truncate_with_ellipsis("", 5), "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use tokio::signal::ctrl_c;
|
|||||||
use tokio_util::task::AbortOnDropHandle;
|
use tokio_util::task::AbortOnDropHandle;
|
||||||
|
|
||||||
pub use self::export::message_to_markdown;
|
pub use self::export::message_to_markdown;
|
||||||
pub use builder::{build_session, SessionBuilderConfig, SessionSettings};
|
pub use builder::{build_session, SessionBuilderConfig};
|
||||||
use console::Color;
|
use console::Color;
|
||||||
use goose::agents::AgentEvent;
|
use goose::agents::AgentEvent;
|
||||||
use goose::permission::permission_confirmation::PrincipalType;
|
use goose::permission::permission_confirmation::PrincipalType;
|
||||||
@@ -255,12 +255,9 @@ impl CliSession {
|
|||||||
&self.session_id
|
&self.session_id
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a stdio extension to the session
|
/// Parse a stdio extension command string into an ExtensionConfig
|
||||||
///
|
/// Format: "ENV1=val1 ENV2=val2 command args..."
|
||||||
/// # Arguments
|
pub fn parse_stdio_extension(extension_command: &str) -> Result<ExtensionConfig> {
|
||||||
/// * `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 mut parts: Vec<&str> = extension_command.split_whitespace().collect();
|
let mut parts: Vec<&str> = extension_command.split_whitespace().collect();
|
||||||
let mut envs = HashMap::new();
|
let mut envs = HashMap::new();
|
||||||
|
|
||||||
@@ -279,94 +276,91 @@ impl CliSession {
|
|||||||
|
|
||||||
let cmd = parts.remove(0).to_string();
|
let cmd = parts.remove(0).to_string();
|
||||||
|
|
||||||
let config = ExtensionConfig::Stdio {
|
Ok(ExtensionConfig::Stdio {
|
||||||
name: String::new(),
|
name: String::new(),
|
||||||
cmd,
|
cmd,
|
||||||
args: parts.iter().map(|s| s.to_string()).collect(),
|
args: parts.iter().map(|s| s.to_string()).collect(),
|
||||||
envs: Envs::new(envs),
|
envs: Envs::new(envs),
|
||||||
env_keys: Vec::new(),
|
env_keys: Vec::new(),
|
||||||
description: goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string(),
|
description: goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string(),
|
||||||
// TODO: should set timeout
|
|
||||||
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
|
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
|
||||||
bundled: None,
|
bundled: None,
|
||||||
available_tools: Vec::new(),
|
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
|
pub fn parse_streamable_http_extension(extension_url: &str) -> ExtensionConfig {
|
||||||
///
|
ExtensionConfig::StreamableHttp {
|
||||||
/// # Arguments
|
|
||||||
/// * `extension_url` - URL of the server
|
|
||||||
pub async fn add_streamable_http_extension(&mut self, extension_url: String) -> Result<()> {
|
|
||||||
let config = ExtensionConfig::StreamableHttp {
|
|
||||||
name: String::new(),
|
name: String::new(),
|
||||||
uri: extension_url,
|
uri: extension_url.to_string(),
|
||||||
envs: Envs::new(HashMap::new()),
|
envs: Envs::new(HashMap::new()),
|
||||||
env_keys: Vec::new(),
|
env_keys: Vec::new(),
|
||||||
headers: HashMap::new(),
|
headers: HashMap::new(),
|
||||||
description: goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string(),
|
description: goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string(),
|
||||||
// TODO: should set timeout
|
|
||||||
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
|
timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT),
|
||||||
bundled: None,
|
bundled: None,
|
||||||
available_tools: Vec::new(),
|
available_tools: Vec::new(),
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse builtin extension names (comma-separated) into ExtensionConfigs
|
||||||
|
pub fn parse_builtin_extensions(builtin_name: &str) -> Vec<ExtensionConfig> {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_and_persist_extensions(&mut self, configs: Vec<ExtensionConfig>) -> Result<()> {
|
||||||
|
for config in configs {
|
||||||
|
self.agent
|
||||||
|
.add_extension(config)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to start extension: {}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
self.agent
|
self.agent
|
||||||
.add_extension(config)
|
.persist_extension_state(&self.session_id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to start extension: {}", e))?;
|
.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;
|
self.invalidate_completion_cache().await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a builtin extension to the session
|
pub async fn add_extension(&mut self, extension_command: String) -> Result<()> {
|
||||||
///
|
let config = Self::parse_stdio_extension(&extension_command)?;
|
||||||
/// # Arguments
|
self.add_and_persist_extensions(vec![config]).await
|
||||||
/// * `builtin_name` - Name of the builtin extension(s), comma separated
|
}
|
||||||
|
|
||||||
|
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_extensions(vec![config]).await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn add_builtin(&mut self, builtin_name: String) -> Result<()> {
|
pub async fn add_builtin(&mut self, builtin_name: String) -> Result<()> {
|
||||||
for name in builtin_name.split(',') {
|
let configs = Self::parse_builtin_extensions(&builtin_name);
|
||||||
let extension_name = name.trim();
|
self.add_and_persist_extensions(configs).await
|
||||||
|
|
||||||
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(),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
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
|
|
||||||
self.invalidate_completion_cache().await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_prompts(
|
pub async fn list_prompts(
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use goose::goose_apps::{fetch_mcp_apps, GooseApp, McpAppCache};
|
|||||||
|
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use goose::agents::ExtensionConfig;
|
use goose::agents::ExtensionConfig;
|
||||||
|
use goose::config::resolve_extensions_for_new_session;
|
||||||
use goose::config::{Config, GooseMode};
|
use goose::config::{Config, GooseMode};
|
||||||
use goose::model::ModelConfig;
|
use goose::model::ModelConfig;
|
||||||
use goose::prompt_template::render_global_file;
|
use goose::prompt_template::render_global_file;
|
||||||
@@ -225,9 +226,11 @@ async fn start_agent(
|
|||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Initialize session with extensions (either overrides from hub or global defaults)
|
let recipe_extensions = original_recipe
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|r| r.extensions.as_deref());
|
||||||
let extensions_to_use =
|
let extensions_to_use =
|
||||||
extension_overrides.unwrap_or_else(goose::config::get_enabled_extensions);
|
resolve_extensions_for_new_session(recipe_extensions, extension_overrides);
|
||||||
let mut extension_data = session.extension_data.clone();
|
let mut extension_data = session.extension_data.clone();
|
||||||
let extensions_state = EnabledExtensionsState::new(extensions_to_use);
|
let extensions_state = EnabledExtensionsState::new(extensions_to_use);
|
||||||
if let Err(e) = extensions_state.to_extension_data(&mut extension_data) {
|
if let Err(e) = extensions_state.to_extension_data(&mut extension_data) {
|
||||||
|
|||||||
@@ -155,3 +155,18 @@ pub fn get_warnings() -> Vec<String> {
|
|||||||
}
|
}
|
||||||
warnings
|
warnings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn resolve_extensions_for_new_session(
|
||||||
|
recipe_extensions: Option<&[ExtensionConfig]>,
|
||||||
|
override_extensions: Option<Vec<ExtensionConfig>>,
|
||||||
|
) -> Vec<ExtensionConfig> {
|
||||||
|
if let Some(exts) = recipe_extensions {
|
||||||
|
return exts.to_vec();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(exts) = override_extensions {
|
||||||
|
return exts;
|
||||||
|
}
|
||||||
|
|
||||||
|
get_enabled_extensions()
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ pub use declarative_providers::DeclarativeProviderConfig;
|
|||||||
pub use experiments::ExperimentManager;
|
pub use experiments::ExperimentManager;
|
||||||
pub use extensions::{
|
pub use extensions::{
|
||||||
get_all_extension_names, get_all_extensions, get_enabled_extensions, get_extension_by_name,
|
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,
|
get_warnings, is_extension_enabled, remove_extension, resolve_extensions_for_new_session,
|
||||||
ExtensionEntry,
|
set_extension, set_extension_enabled, ExtensionEntry,
|
||||||
};
|
};
|
||||||
pub use goose_mode::GooseMode;
|
pub use goose_mode::GooseMode;
|
||||||
pub use permission::PermissionManager;
|
pub use permission::PermissionManager;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use tokio_util::sync::CancellationToken;
|
|||||||
use crate::agents::AgentEvent;
|
use crate::agents::AgentEvent;
|
||||||
use crate::agents::{Agent, SessionConfig};
|
use crate::agents::{Agent, SessionConfig};
|
||||||
use crate::config::paths::Paths;
|
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::message::Message;
|
||||||
use crate::conversation::Conversation;
|
use crate::conversation::Conversation;
|
||||||
use crate::posthog;
|
use crate::posthog;
|
||||||
@@ -743,10 +743,9 @@ async fn execute_job(
|
|||||||
|
|
||||||
let agent_provider = create(&provider_name, model_config).await?;
|
let agent_provider = create(&provider_name, model_config).await?;
|
||||||
|
|
||||||
if let Some(ref extensions) = recipe.extensions {
|
let extensions = resolve_extensions_for_new_session(recipe.extensions.as_deref(), None);
|
||||||
for ext in extensions {
|
for ext in extensions {
|
||||||
agent.add_extension(ext.clone()).await?;
|
agent.add_extension(ext.clone()).await?;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let session = agent
|
let session = agent
|
||||||
|
|||||||
Reference in New Issue
Block a user