diff --git a/crates/goose-cli/src/recipes/extract_from_cli.rs b/crates/goose-cli/src/recipes/extract_from_cli.rs index e5af27864242..3c5c738804e6 100644 --- a/crates/goose-cli/src/recipes/extract_from_cli.rs +++ b/crates/goose-cli/src/recipes/extract_from_cli.rs @@ -240,18 +240,26 @@ settings: temperature: 0.7 sub_recipes: - path: existing_sub_recipe.yaml - name: existing_sub_recipe + name: existing_sub_recipe response: json_schema: type: object properties: result: type: string +"#; + let sub_recipe_content = r#" +title: existing_sub_recipe +description: An existing sub recipe +instructions: sub recipe instructions +prompt: sub recipe prompt "#; let temp_dir = tempfile::tempdir().unwrap(); let recipe_path: std::path::PathBuf = temp_dir.path().join("test_recipe.yaml"); + let sub_recipe_path: std::path::PathBuf = temp_dir.path().join("existing_sub_recipe.yaml"); std::fs::write(&recipe_path, test_recipe_content).unwrap(); + std::fs::write(&sub_recipe_path, sub_recipe_content).unwrap(); let canonical_recipe_path = recipe_path.canonicalize().unwrap(); (temp_dir, canonical_recipe_path) } diff --git a/crates/goose/src/recipe/build_recipe/mod.rs b/crates/goose/src/recipe/build_recipe/mod.rs index 33648a839c5f..ea4305dd967f 100644 --- a/crates/goose/src/recipe/build_recipe/mod.rs +++ b/crates/goose/src/recipe/build_recipe/mod.rs @@ -70,9 +70,7 @@ where if let Some(ref mut sub_recipes) = recipe.sub_recipes { for sub_recipe in sub_recipes { - if let Ok(resolved_path) = resolve_sub_recipe_path(&sub_recipe.path, recipe_dir) { - sub_recipe.path = resolved_path; - } + sub_recipe.path = resolve_sub_recipe_path(&sub_recipe.path, recipe_dir)?; } } @@ -122,18 +120,17 @@ fn resolve_sub_recipe_path( parent_recipe_dir: &Path, ) -> Result { let path = if Path::new(sub_recipe_path).is_absolute() { - sub_recipe_path.to_string() + Path::new(sub_recipe_path).to_path_buf() } else { - parent_recipe_dir - .join(sub_recipe_path) - .to_str() - .ok_or_else(|| RecipeError::RecipeParsing { - source: anyhow::anyhow!("Invalid sub-recipe path: {}", sub_recipe_path), - })? - .to_string() + parent_recipe_dir.join(sub_recipe_path) }; + if !path.exists() { + return Err(RecipeError::RecipeParsing { + source: anyhow::anyhow!("Sub-recipe file does not exist: {}", path.display()), + }); + } - Ok(path) + Ok(path.display().to_string()) } #[cfg(test)] diff --git a/crates/goose/src/recipe/build_recipe/tests.rs b/crates/goose/src/recipe/build_recipe/tests.rs index 480fc954f2f3..c59dd4598094 100644 --- a/crates/goose/src/recipe/build_recipe/tests.rs +++ b/crates/goose/src/recipe/build_recipe/tests.rs @@ -435,6 +435,14 @@ mod sub_recipe_path_resolution { let temp_dir = tempfile::tempdir().unwrap(); let parent_dir = temp_dir.path(); + // Create the sub-recipe file + let sub_recipe_content = r#" +version: 1.0.0 +title: Child Recipe +description: A child recipe +instructions: Child instructions"#; + create_recipe_file(parent_dir, "sub-recipes", "child.yaml", sub_recipe_content); + let result = resolve_sub_recipe_path("./sub-recipes/child.yaml", parent_dir); assert!(result.is_ok()); @@ -446,11 +454,37 @@ mod sub_recipe_path_resolution { fn test_resolve_sub_recipe_path_absolute() { let temp_dir = tempfile::tempdir().unwrap(); let parent_dir = temp_dir.path(); - let absolute_path = "/absolute/path/to/recipe.yaml"; - let result = resolve_sub_recipe_path(absolute_path, parent_dir); + let sub_recipe_content = r#" +version: 1.0.0 +title: Absolute Recipe +description: A recipe with absolute path +instructions: Absolute instructions"#; + let absolute_path = + create_recipe_file(parent_dir, "absolute", "recipe.yaml", sub_recipe_content); + let absolute_path_str = absolute_path.to_str().unwrap(); + + let result = resolve_sub_recipe_path(absolute_path_str, parent_dir); assert!(result.is_ok()); - assert_eq!(result.unwrap(), absolute_path); + assert_eq!(result.unwrap(), absolute_path_str); + } + + #[test] + fn test_resolve_sub_recipe_path_nonexistent() { + let temp_dir = tempfile::tempdir().unwrap(); + let parent_dir = temp_dir.path(); + + let result = resolve_sub_recipe_path("./sub-recipes/nonexistent.yaml", parent_dir); + + assert!(result.is_err()); + match result { + Err(RecipeError::RecipeParsing { source }) => { + let error_msg = source.to_string(); + assert!(error_msg.contains("Sub-recipe file does not exist")); + assert!(error_msg.contains("nonexistent.yaml")); + } + _ => panic!("Expected RecipeError::RecipeParsing"), + } } #[test] diff --git a/ui/desktop/src/hooks/useRecipeManager.ts b/ui/desktop/src/hooks/useRecipeManager.ts index cdf24c783271..f83274638323 100644 --- a/ui/desktop/src/hooks/useRecipeManager.ts +++ b/ui/desktop/src/hooks/useRecipeManager.ts @@ -7,7 +7,7 @@ import { substituteParameters } from '../utils/providerUtils'; import { updateSessionUserRecipeValues } from '../api'; import { useChatContext } from '../contexts/ChatContext'; import { ChatType } from '../types/chat'; -import { toastSuccess } from '../toasts'; +import { toastError, toastSuccess } from '../toasts'; export const useRecipeManager = (chat: ChatType, recipe?: Recipe | null) => { const [isParameterModalOpen, setIsParameterModalOpen] = useState(false); @@ -181,7 +181,17 @@ export const useRecipeManager = (chat: ChatType, recipe?: Recipe | null) => { } setIsParameterModalOpen(false); } catch (error) { - console.error('Failed to update system prompt with parameters:', error); + let error_message = 'unknown error'; + if (typeof error === 'object' && error !== null && 'message' in error) { + error_message = error.message as string; + } else if (typeof error === 'string') { + error_message = error; + } + console.error('Failed to render recipe with parameters:', error); + toastError({ + title: 'Recipe Rendering Failed', + msg: error_message, + }); } };