Goose recipes have settings now (#2397)

Co-authored-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: Lifei Zhou <lifei@squareup.com>
This commit is contained in:
Douwe Osinga
2025-06-12 21:32:01 -04:00
committed by GitHub
parent 9945e51d73
commit 78afaded33
8 changed files with 110 additions and 30 deletions

View File

@@ -19,7 +19,7 @@ use crate::commands::session::{handle_session_list, handle_session_remove};
use crate::logging::setup_logging; use crate::logging::setup_logging;
use crate::recipes::recipe::{explain_recipe_with_parameters, load_recipe_as_template}; use crate::recipes::recipe::{explain_recipe_with_parameters, load_recipe_as_template};
use crate::session; use crate::session;
use crate::session::{build_session, SessionBuilderConfig}; use crate::session::{build_session, SessionBuilderConfig, SessionSettings};
use goose_bench::bench_config::BenchRunConfig; use goose_bench::bench_config::BenchRunConfig;
use goose_bench::runners::bench_runner::BenchRunner; use goose_bench::runners::bench_runner::BenchRunner;
use goose_bench::runners::eval_runner::EvalRunner; use goose_bench::runners::eval_runner::EvalRunner;
@@ -552,6 +552,7 @@ enum CliProviderVariant {
Ollama, Ollama,
} }
#[derive(Debug)]
struct InputConfig { struct InputConfig {
contents: Option<String>, contents: Option<String>,
extensions_override: Option<Vec<ExtensionConfig>>, extensions_override: Option<Vec<ExtensionConfig>>,
@@ -630,6 +631,7 @@ pub async fn cli() -> Result<()> {
builtins, builtins,
extensions_override: None, extensions_override: None,
additional_system_prompt: None, additional_system_prompt: None,
settings: None,
debug, debug,
max_tool_repetitions, max_tool_repetitions,
interactive: true, // Session command is always interactive interactive: true, // Session command is always interactive
@@ -676,18 +678,22 @@ pub async fn cli() -> Result<()> {
params, params,
explain, explain,
}) => { }) => {
let input_config = match (instructions, input_text, recipe, explain) { let (input_config, session_settings) = match (instructions, input_text, recipe, explain)
{
(Some(file), _, _, _) if file == "-" => { (Some(file), _, _, _) if file == "-" => {
let mut input = String::new(); let mut input = String::new();
std::io::stdin() std::io::stdin()
.read_to_string(&mut input) .read_to_string(&mut input)
.expect("Failed to read from stdin"); .expect("Failed to read from stdin");
InputConfig { (
contents: Some(input), InputConfig {
extensions_override: None, contents: Some(input),
additional_system_prompt: None, extensions_override: None,
} additional_system_prompt: None,
},
None,
)
} }
(Some(file), _, _, _) => { (Some(file), _, _, _) => {
let contents = std::fs::read_to_string(&file).unwrap_or_else(|err| { let contents = std::fs::read_to_string(&file).unwrap_or_else(|err| {
@@ -697,17 +703,23 @@ pub async fn cli() -> Result<()> {
); );
std::process::exit(1); std::process::exit(1);
}); });
(
InputConfig {
contents: Some(contents),
extensions_override: None,
additional_system_prompt: None,
},
None,
)
}
(_, Some(text), _, _) => (
InputConfig { InputConfig {
contents: Some(contents), contents: Some(text),
extensions_override: None, extensions_override: None,
additional_system_prompt: None, additional_system_prompt: None,
} },
} None,
(_, Some(text), _, _) => InputConfig { ),
contents: Some(text),
extensions_override: None,
additional_system_prompt: None,
},
(_, _, Some(recipe_name), explain) => { (_, _, Some(recipe_name), explain) => {
if explain { if explain {
explain_recipe_with_parameters(&recipe_name, params)?; explain_recipe_with_parameters(&recipe_name, params)?;
@@ -718,11 +730,18 @@ pub async fn cli() -> Result<()> {
eprintln!("{}: {}", console::style("Error").red().bold(), err); eprintln!("{}: {}", console::style("Error").red().bold(), err);
std::process::exit(1); std::process::exit(1);
}); });
InputConfig { (
contents: recipe.prompt, InputConfig {
extensions_override: recipe.extensions, contents: recipe.prompt,
additional_system_prompt: recipe.instructions, extensions_override: recipe.extensions,
} additional_system_prompt: recipe.instructions,
},
recipe.settings.map(|s| SessionSettings {
goose_provider: s.goose_provider,
goose_model: s.goose_model,
temperature: s.temperature,
}),
)
} }
(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.");
@@ -739,6 +758,7 @@ pub async fn cli() -> Result<()> {
builtins, builtins,
extensions_override: input_config.extensions_override, extensions_override: input_config.extensions_override,
additional_system_prompt: input_config.additional_system_prompt, additional_system_prompt: input_config.additional_system_prompt,
settings: session_settings,
debug, debug,
max_tool_repetitions, max_tool_repetitions,
interactive, // Use the interactive flag from the Run command interactive, // Use the interactive flag from the Run command
@@ -854,6 +874,7 @@ pub async fn cli() -> Result<()> {
builtins: Vec::new(), builtins: Vec::new(),
extensions_override: None, extensions_override: None,
additional_system_prompt: None, additional_system_prompt: None,
settings: None::<SessionSettings>,
debug: false, debug: false,
max_tool_repetitions: None, max_tool_repetitions: None,
interactive: true, // Default case is always interactive interactive: true, // Default case is always interactive

View File

@@ -40,6 +40,7 @@ pub async fn agent_generator(
builtins: requirements.builtin, builtins: requirements.builtin,
extensions_override: None, extensions_override: None,
additional_system_prompt: None, additional_system_prompt: None,
settings: None,
debug: false, debug: false,
max_tool_repetitions: None, max_tool_repetitions: None,
interactive: false, // Benchmarking is non-interactive interactive: false, // Benchmarking is non-interactive

View File

@@ -34,6 +34,8 @@ pub struct SessionBuilderConfig {
pub extensions_override: Option<Vec<ExtensionConfig>>, pub extensions_override: Option<Vec<ExtensionConfig>>,
/// 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>,
/// Enable debug printing /// Enable debug printing
pub debug: bool, pub debug: bool,
/// Maximum number of consecutive identical tool calls allowed /// Maximum number of consecutive identical tool calls allowed
@@ -136,18 +138,35 @@ async fn offer_extension_debugging_help(
Ok(()) Ok(())
} }
#[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) -> Session { pub async fn build_session(session_config: SessionBuilderConfig) -> Session {
// Load config and get provider/model // Load config and get provider/model
let config = Config::global(); let config = Config::global();
let provider_name: String = config let provider_name = session_config
.get_param("GOOSE_PROVIDER") .settings
.as_ref()
.and_then(|s| s.goose_provider.clone())
.or_else(|| config.get_param("GOOSE_PROVIDER").ok())
.expect("No provider configured. Run 'goose configure' first"); .expect("No provider configured. Run 'goose configure' first");
let model: String = config let model_name = session_config
.get_param("GOOSE_MODEL") .settings
.as_ref()
.and_then(|s| s.goose_model.clone())
.or_else(|| config.get_param("GOOSE_MODEL").ok())
.expect("No model configured. Run 'goose configure' first"); .expect("No model configured. Run 'goose configure' first");
let model_config = goose::model::ModelConfig::new(model.clone());
let temperature = session_config.settings.as_ref().and_then(|s| s.temperature);
let model_config =
goose::model::ModelConfig::new(model_name.clone()).with_temperature(temperature);
// Create the agent // Create the agent
let agent: Agent = Agent::new(); let agent: Agent = Agent::new();
@@ -165,7 +184,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> Session {
worker_model worker_model
); );
} else { } else {
tracing::info!("🤖 Using model: {}", model); tracing::info!("🤖 Using model: {}", model_name);
} }
agent agent
@@ -430,7 +449,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> Session {
output::display_session_info( output::display_session_info(
session_config.resume, session_config.resume,
&provider_name, &provider_name,
&model, &model_name,
&session_file, &session_file,
Some(&provider_for_display), Some(&provider_for_display),
); );
@@ -452,6 +471,7 @@ mod tests {
builtins: vec!["developer".to_string()], builtins: vec!["developer".to_string()],
extensions_override: None, extensions_override: None,
additional_system_prompt: Some("Test prompt".to_string()), additional_system_prompt: Some("Test prompt".to_string()),
settings: None,
debug: true, debug: true,
max_tool_repetitions: Some(5), max_tool_repetitions: Some(5),
interactive: true, interactive: true,

View File

@@ -7,7 +7,7 @@ mod prompt;
mod thinking; mod thinking;
pub use self::export::message_to_markdown; pub use self::export::message_to_markdown;
pub use builder::{build_session, SessionBuilderConfig}; pub use builder::{build_session, SessionBuilderConfig, SessionSettings};
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;

View File

@@ -16,7 +16,7 @@ use crate::permission::permission_judge::check_tool_permissions;
use crate::permission::PermissionConfirmation; use crate::permission::PermissionConfirmation;
use crate::providers::base::Provider; use crate::providers::base::Provider;
use crate::providers::errors::ProviderError; use crate::providers::errors::ProviderError;
use crate::recipe::{Author, Recipe}; use crate::recipe::{Author, Recipe, Settings};
use crate::tool_monitor::{ToolCall, ToolMonitor}; use crate::tool_monitor::{ToolCall, ToolMonitor};
use regex::Regex; use regex::Regex;
use serde_json::Value; use serde_json::Value;
@@ -973,12 +973,26 @@ impl Agent {
metadata: None, metadata: None,
}; };
// Ideally we'd get the name of the provider we are using from the provider itself
// but it doesn't know and the plumbing looks complicated.
let config = Config::global();
let provider_name: String = config
.get_param("GOOSE_PROVIDER")
.expect("No provider configured. Run 'goose configure' first");
let settings = Settings {
goose_provider: Some(provider_name.clone()),
goose_model: Some(model_name.clone()),
temperature: Some(model_config.temperature.unwrap_or(0.0)),
};
let recipe = Recipe::builder() let recipe = Recipe::builder()
.title("Custom recipe from chat") .title("Custom recipe from chat")
.description("a custom recipe instance from this chat session") .description("a custom recipe instance from this chat session")
.instructions(instructions) .instructions(instructions)
.activities(activities) .activities(activities)
.extensions(extension_configs) .extensions(extension_configs)
.settings(settings)
.author(author) .author(author)
.build() .build()
.expect("valid recipe"); .expect("valid recipe");

View File

@@ -154,7 +154,6 @@ impl PromptManager {
} }
} }
/// Get the recipe prompt
pub async fn get_recipe_prompt(&self) -> String { pub async fn get_recipe_prompt(&self) -> String {
let context: HashMap<&str, Value> = HashMap::new(); let context: HashMap<&str, Value> = HashMap::new();
prompt_template::render_global_file("recipe.md", &context).expect("Prompt should render") prompt_template::render_global_file("recipe.md", &context).expect("Prompt should render")

View File

@@ -50,6 +50,7 @@ fn default_version() -> String {
/// context: None, /// context: None,
/// activities: None, /// activities: None,
/// author: None, /// author: None,
/// settings: None,
/// parameters: None, /// parameters: None,
/// }; /// };
/// ///
@@ -77,6 +78,9 @@ pub struct Recipe {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<Vec<String>>, // any additional context pub context: Option<Vec<String>>, // any additional context
#[serde(skip_serializing_if = "Option::is_none")]
pub settings: Option<Settings>, // settings for the recipe
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub activities: Option<Vec<String>>, // the activity pills that show up when loading the pub activities: Option<Vec<String>>, // the activity pills that show up when loading the
@@ -96,6 +100,18 @@ pub struct Author {
pub metadata: Option<String>, // any additional metadata for the author pub metadata: Option<String>, // any additional metadata for the author
} }
#[derive(Serialize, Deserialize, Debug)]
pub struct Settings {
#[serde(skip_serializing_if = "Option::is_none")]
pub goose_provider: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub goose_model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
}
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum RecipeParameterRequirement { pub enum RecipeParameterRequirement {
@@ -156,6 +172,7 @@ pub struct RecipeBuilder {
prompt: Option<String>, prompt: Option<String>,
extensions: Option<Vec<ExtensionConfig>>, extensions: Option<Vec<ExtensionConfig>>,
context: Option<Vec<String>>, context: Option<Vec<String>>,
settings: Option<Settings>,
activities: Option<Vec<String>>, activities: Option<Vec<String>>,
author: Option<Author>, author: Option<Author>,
parameters: Option<Vec<RecipeParameter>>, parameters: Option<Vec<RecipeParameter>>,
@@ -185,6 +202,7 @@ impl Recipe {
prompt: None, prompt: None,
extensions: None, extensions: None,
context: None, context: None,
settings: None,
activities: None, activities: None,
author: None, author: None,
parameters: None, parameters: None,
@@ -234,6 +252,11 @@ impl RecipeBuilder {
self self
} }
pub fn settings(mut self, settings: Settings) -> Self {
self.settings = Some(settings);
self
}
/// Sets the activities for the Recipe /// Sets the activities for the Recipe
pub fn activities(mut self, activities: Vec<String>) -> Self { pub fn activities(mut self, activities: Vec<String>) -> Self {
self.activities = Some(activities); self.activities = Some(activities);
@@ -271,6 +294,7 @@ impl RecipeBuilder {
prompt: self.prompt, prompt: self.prompt,
extensions: self.extensions, extensions: self.extensions,
context: self.context, context: self.context,
settings: self.settings,
activities: self.activities, activities: self.activities,
author: self.author, author: self.author,
parameters: self.parameters, parameters: self.parameters,

View File

@@ -1300,6 +1300,7 @@ mod tests {
activities: None, activities: None,
author: None, author: None,
parameters: None, parameters: None,
settings: None,
}; };
let mut recipe_file = File::create(&recipe_filename)?; let mut recipe_file = File::create(&recipe_filename)?;
writeln!( writeln!(