From f1483efa825040362bc0c74dcaeedb41bdfd24e3 Mon Sep 17 00:00:00 2001 From: Tyler Longwell Date: Thu, 5 Feb 2026 15:01:32 -0500 Subject: [PATCH] Auto-inject summon extension for recipes with sub_recipes When a recipe defines sub_recipes, the summon extension is now automatically added to the extensions list. This ensures the delegate tool is available without requiring users to manually specify the summon extension. Changes: - Add ensure_summon_for_subrecipes() helper in Recipe that auto-injects the summon platform extension when sub_recipes are present - Update test assertions to expect summon in extension list - Update test script to look for 'delegate' instead of 'subagent' --- .../goose-cli/src/recipes/extract_from_cli.rs | 10 ++++-- crates/goose/src/recipe/mod.rs | 33 ++++++++++++++++--- .../project_analyzer_parallel.yaml | 1 + scripts/test_subrecipes.sh | 18 +++++----- 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/crates/goose-cli/src/recipes/extract_from_cli.rs b/crates/goose-cli/src/recipes/extract_from_cli.rs index aac9e91cea..3c8420c787 100644 --- a/crates/goose-cli/src/recipes/extract_from_cli.rs +++ b/crates/goose-cli/src/recipes/extract_from_cli.rs @@ -97,7 +97,10 @@ mod tests { input_config.additional_system_prompt, Some("test_instructions my_value".to_string()) ); - assert!(recipe.extensions.is_none()); + assert!(recipe + .extensions + .as_ref() + .is_none_or(|e| e.iter().all(|ext| ext.name() == "summon"))); assert!(settings.is_some()); let settings = settings.unwrap(); @@ -162,7 +165,10 @@ mod tests { input_config.additional_system_prompt, Some("test_instructions my_value".to_string()) ); - assert!(recipe.extensions.is_none()); + assert!(recipe + .extensions + .as_ref() + .is_none_or(|e| e.iter().all(|ext| ext.name() == "summon"))); assert!(settings.is_some()); let settings = settings.unwrap(); diff --git a/crates/goose/src/recipe/mod.rs b/crates/goose/src/recipe/mod.rs index 1dd3cb12fb..b47192a99e 100644 --- a/crates/goose/src/recipe/mod.rs +++ b/crates/goose/src/recipe/mod.rs @@ -226,6 +226,24 @@ pub struct RecipeBuilder { } impl Recipe { + fn ensure_summon_for_subrecipes(&mut self) { + if self.sub_recipes.is_none() { + return; + } + let summon = ExtensionConfig::Platform { + name: "summon".to_string(), + description: String::new(), + display_name: None, + bundled: None, + available_tools: vec![], + }; + match &mut self.extensions { + Some(exts) if !exts.iter().any(|e| e.name() == "summon") => exts.push(summon), + None => self.extensions = Some(vec![summon]), + _ => {} + } + } + /// Returns true if harmful content is detected in instructions, prompt, or activities fields pub fn check_for_security_warnings(&self) -> bool { if [self.instructions.as_deref(), self.prompt.as_deref()] @@ -277,7 +295,7 @@ impl Recipe { } pub fn from_content(content: &str) -> Result { - let recipe: Recipe = match serde_yaml::from_str::(content) { + let mut recipe: Recipe = match serde_yaml::from_str::(content) { Ok(yaml_value) => { if let Some(nested_recipe) = yaml_value.get("recipe") { serde_yaml::from_value(nested_recipe.clone()) @@ -291,6 +309,7 @@ impl Recipe { .map_err(|e| anyhow::anyhow!("{}", strip_error_location(&e.to_string())))?, }; + recipe.ensure_summon_for_subrecipes(); Ok(recipe) } } @@ -450,8 +469,10 @@ mod tests { assert_eq!(recipe.prompt, Some("Test prompt".to_string())); assert!(recipe.extensions.is_some()); - let extensions = recipe.extensions.unwrap(); - assert_eq!(extensions.len(), 1); + let extensions = recipe.extensions.as_ref().unwrap(); + assert_eq!(extensions.len(), 2); + assert!(extensions.iter().any(|e| e.name() == "test_extension")); + assert!(extensions.iter().any(|e| e.name() == "summon")); assert!(recipe.parameters.is_some()); let parameters = recipe.parameters.unwrap(); @@ -533,8 +554,10 @@ sub_recipes: assert_eq!(recipe.prompt, Some("Test prompt".to_string())); assert!(recipe.extensions.is_some()); - let extensions = recipe.extensions.unwrap(); - assert_eq!(extensions.len(), 1); + let extensions = recipe.extensions.as_ref().unwrap(); + assert_eq!(extensions.len(), 2); + assert!(extensions.iter().any(|e| e.name() == "test_extension")); + assert!(extensions.iter().any(|e| e.name() == "summon")); assert!(recipe.parameters.is_some()); let parameters = recipe.parameters.unwrap(); diff --git a/scripts/test-subrecipes-examples/project_analyzer_parallel.yaml b/scripts/test-subrecipes-examples/project_analyzer_parallel.yaml index 7ea4c3a27a..f05d8fc7a4 100644 --- a/scripts/test-subrecipes-examples/project_analyzer_parallel.yaml +++ b/scripts/test-subrecipes-examples/project_analyzer_parallel.yaml @@ -17,6 +17,7 @@ prompt: | Run two subrecipes in parallel: - use file_stats subrecipe to gather file statistics for {{ target_directory }} - use code_patterns subrecipe to analyze code patterns in {{ target_directory }} + Iteratively `sleep 10` until the delegates complete, then load their output and confirm success extensions: - type: builtin name: developer diff --git a/scripts/test_subrecipes.sh b/scripts/test_subrecipes.sh index 25d5e11190..30a4bb03a4 100755 --- a/scripts/test_subrecipes.sh +++ b/scripts/test_subrecipes.sh @@ -22,7 +22,7 @@ export PATH="$SCRIPT_DIR/target/release:$PATH" # Set default provider and model if not already set # Use fast model for CI to speed up tests export GOOSE_PROVIDER="${GOOSE_PROVIDER:-anthropic}" -export GOOSE_MODEL="${GOOSE_MODEL:-claude-3-5-haiku-20241022}" +export GOOSE_MODEL="${GOOSE_MODEL:-claude-haiku-4-5}" echo "Using provider: $GOOSE_PROVIDER" echo "Using model: $GOOSE_MODEL" @@ -77,17 +77,17 @@ check_recipe_output() { local tmpfile=$1 local mode=$2 - # Check for unified subagent tool invocation (new format: "─── subagent |") - if grep -q "─── subagent" "$tmpfile"; then - echo "✓ SUCCESS: Subagent tool invoked" - RESULTS+=("✓ Subagent tool invocation ($mode)") + # Check for delegate tool invocation (new format: "─── delegate |") + if grep -q "─── delegate" "$tmpfile"; then + echo "✓ SUCCESS: Delegate tool invoked" + RESULTS+=("✓ Delegate tool invocation ($mode)") else - echo "✗ FAILED: No evidence of subagent tool invocation" - RESULTS+=("✗ Subagent tool invocation ($mode)") + echo "✗ FAILED: No evidence of delegate tool invocation" + RESULTS+=("✗ Delegate tool invocation ($mode)") fi - # Check that both subrecipes were called (shown as "subrecipe: " in output) - if grep -q "subrecipe:.*file_stats\|file_stats.*subrecipe" "$tmpfile" && grep -q "subrecipe:.*code_patterns\|code_patterns.*subrecipe" "$tmpfile"; then + # Check that both subrecipes were called (shown as "source: " in delegate output) + if grep -q "source:.*file_stats\|source.*file_stats" "$tmpfile" && grep -q "source:.*code_patterns\|source.*code_patterns" "$tmpfile"; then echo "✓ SUCCESS: Both subrecipes (file_stats, code_patterns) found in output" RESULTS+=("✓ Both subrecipes present ($mode)") else