feat: recipes can retry with success criteria (#3474)

This commit is contained in:
Prem Pillai
2025-07-22 10:49:21 +10:00
committed by GitHub
parent 5f3c7d339c
commit 99cc0a9c81
17 changed files with 1078 additions and 82 deletions
+39 -36
View File
@@ -685,6 +685,14 @@ pub struct InputConfig {
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>,
}
pub async fn cli() -> Result<()> {
let cli = Cli::parse();
@@ -771,6 +779,7 @@ pub async fn cli() -> Result<()> {
quiet: false,
sub_recipes: None,
final_output_response: None,
retry_config: None,
})
.await;
setup_logging(
@@ -828,27 +837,19 @@ pub async fn cli() -> Result<()> {
provider,
model,
}) => {
let (input_config, session_settings, sub_recipes, final_output_response) = match (
instructions,
input_text,
recipe,
) {
let (input_config, recipe_info) = match (instructions, input_text, recipe) {
(Some(file), _, _) if file == "-" => {
let mut input = String::new();
std::io::stdin()
.read_to_string(&mut input)
.expect("Failed to read from stdin");
(
InputConfig {
contents: Some(input),
extensions_override: None,
additional_system_prompt: system,
},
None,
None,
None,
)
let input_config = InputConfig {
contents: Some(input),
extensions_override: None,
additional_system_prompt: system,
};
(input_config, None)
}
(Some(file), _, _) => {
let contents = std::fs::read_to_string(&file).unwrap_or_else(|err| {
@@ -858,27 +859,21 @@ pub async fn cli() -> Result<()> {
);
std::process::exit(1);
});
(
InputConfig {
contents: Some(contents),
extensions_override: None,
additional_system_prompt: None,
},
None,
None,
None,
)
let input_config = InputConfig {
contents: Some(contents),
extensions_override: None,
additional_system_prompt: None,
};
(input_config, None)
}
(_, Some(text), _) => (
InputConfig {
(_, Some(text), _) => {
let input_config = InputConfig {
contents: Some(text),
extensions_override: None,
additional_system_prompt: system,
},
None,
None,
None,
),
};
(input_config, None)
}
(_, _, Some(recipe_name)) => {
if explain {
explain_recipe(&recipe_name, params)?;
@@ -891,7 +886,9 @@ pub async fn cli() -> Result<()> {
}
return Ok(());
}
extract_recipe_info_from_cli(recipe_name, params, additional_sub_recipes)?
let (input_config, recipe_info) =
extract_recipe_info_from_cli(recipe_name, params, additional_sub_recipes)?;
(input_config, Some(recipe_info))
}
(None, None, None) => {
eprintln!("Error: Must provide either --instructions (-i), --text (-t), or --recipe. Use -i - for stdin.");
@@ -909,7 +906,9 @@ pub async fn cli() -> Result<()> {
builtins,
extensions_override: input_config.extensions_override,
additional_system_prompt: input_config.additional_system_prompt,
settings: session_settings,
settings: recipe_info
.as_ref()
.and_then(|r| r.session_settings.clone()),
provider,
model,
debug,
@@ -918,8 +917,11 @@ pub async fn cli() -> Result<()> {
scheduled_job_id,
interactive, // Use the interactive flag from the Run command
quiet,
sub_recipes,
final_output_response,
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()),
})
.await;
@@ -1051,6 +1053,7 @@ pub async fn cli() -> Result<()> {
quiet: false,
sub_recipes: None,
final_output_response: None,
retry_config: None,
})
.await;
setup_logging(
+1
View File
@@ -52,6 +52,7 @@ pub async fn agent_generator(
quiet: false,
sub_recipes: None,
final_output_response: None,
retry_config: None,
})
.await;
+1
View File
@@ -491,6 +491,7 @@ async fn process_message_streaming(
schedule_id: None,
execution_mode: None,
max_turns: None,
retry_config: None,
};
// Get response from agent
@@ -1,24 +1,21 @@
use std::path::PathBuf;
use anyhow::{anyhow, Result};
use goose::recipe::{Response, SubRecipe};
use goose::recipe::SubRecipe;
use crate::recipes::print_recipe::print_recipe_info;
use crate::recipes::recipe::load_recipe;
use crate::recipes::search_recipe::retrieve_recipe_file;
use crate::{cli::InputConfig, session::SessionSettings};
use crate::{
cli::{InputConfig, RecipeInfo},
session::SessionSettings,
};
#[allow(clippy::type_complexity)]
pub fn extract_recipe_info_from_cli(
recipe_name: String,
params: Vec<(String, String)>,
additional_sub_recipes: Vec<String>,
) -> Result<(
InputConfig,
Option<SessionSettings>,
Option<Vec<SubRecipe>>,
Option<Response>,
)> {
) -> Result<(InputConfig, RecipeInfo)> {
let recipe = load_recipe(&recipe_name, params.clone()).unwrap_or_else(|err| {
eprintln!("{}: {}", console::style("Error").red().bold(), err);
std::process::exit(1);
@@ -49,20 +46,24 @@ pub fn extract_recipe_info_from_cli(
}
}
}
Ok((
InputConfig {
contents: recipe.prompt.filter(|s| !s.trim().is_empty()),
extensions_override: recipe.extensions,
additional_system_prompt: recipe.instructions,
},
recipe.settings.map(|s| SessionSettings {
let input_config = InputConfig {
contents: recipe.prompt.filter(|s| !s.trim().is_empty()),
extensions_override: recipe.extensions,
additional_system_prompt: recipe.instructions,
};
let recipe_info = RecipeInfo {
session_settings: recipe.settings.map(|s| SessionSettings {
goose_provider: s.goose_provider,
goose_model: s.goose_model,
temperature: s.temperature,
}),
Some(all_sub_recipes),
recipe.response,
))
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 {
@@ -93,8 +94,11 @@ mod tests {
let params = vec![("name".to_string(), "my_value".to_string())];
let recipe_name = recipe_path.to_str().unwrap().to_string();
let (input_config, settings, sub_recipes, response) =
let (input_config, recipe_info) =
extract_recipe_info_from_cli(recipe_name, params, Vec::new()).unwrap();
let settings = recipe_info.session_settings;
let sub_recipes = recipe_info.sub_recipes;
let response = recipe_info.final_output_response;
assert_eq!(input_config.contents, Some("test_prompt".to_string()));
assert_eq!(
@@ -149,8 +153,11 @@ mod tests {
sub_recipe2_path.to_string_lossy().to_string(),
];
let (input_config, settings, sub_recipes, response) =
let (input_config, recipe_info) =
extract_recipe_info_from_cli(recipe_name, params, additional_sub_recipes).unwrap();
let settings = recipe_info.session_settings;
let sub_recipes = recipe_info.sub_recipes;
let response = recipe_info.final_output_response;
assert_eq!(input_config.contents, Some("test_prompt".to_string()));
assert_eq!(
+6
View File
@@ -1,5 +1,6 @@
use console::style;
use goose::agents::extension::ExtensionError;
use goose::agents::types::RetryConfig;
use goose::agents::Agent;
use goose::config::{Config, ExtensionConfig, ExtensionConfigManager};
use goose::providers::create;
@@ -60,6 +61,8 @@ pub struct SessionBuilderConfig {
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>,
}
/// Offers to help debug an extension failure by creating a minimal debugging session
@@ -138,6 +141,7 @@ async fn offer_extension_debugging_help(
None,
None,
None,
None,
);
// Process the debugging request
@@ -407,6 +411,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> Session {
session_config.scheduled_job_id.clone(),
session_config.max_turns,
edit_mode,
session_config.retry_config.clone(),
);
// Add extensions if provided
@@ -602,6 +607,7 @@ mod tests {
quiet: false,
sub_recipes: None,
final_output_response: None,
retry_config: None,
};
assert_eq!(config.extensions.len(), 1);
+5
View File
@@ -28,6 +28,7 @@ use anyhow::{Context, Result};
use completion::GooseCompleter;
use etcetera::{choose_app_strategy, AppStrategy};
use goose::agents::extension::{Envs, ExtensionConfig};
use goose::agents::types::RetryConfig;
use goose::agents::{Agent, SessionConfig};
use goose::config::Config;
use goose::message::{Message, MessageContent};
@@ -64,6 +65,7 @@ pub struct Session {
scheduled_job_id: Option<String>, // ID of the scheduled job that triggered this session
max_turns: Option<u32>,
edit_mode: Option<EditMode>,
retry_config: Option<RetryConfig>,
}
// Cache structure for completion data
@@ -127,6 +129,7 @@ impl Session {
scheduled_job_id: Option<String>,
max_turns: Option<u32>,
edit_mode: Option<EditMode>,
retry_config: Option<RetryConfig>,
) -> Self {
let messages = if let Some(session_file) = &session_file {
match session::read_messages(session_file) {
@@ -151,6 +154,7 @@ impl Session {
scheduled_job_id,
max_turns,
edit_mode,
retry_config,
}
}
@@ -879,6 +883,7 @@ impl Session {
schedule_id: self.scheduled_job_id.clone(),
execution_mode: None,
max_turns: self.max_turns,
retry_config: self.retry_config.clone(),
}
});
let mut stream = self