Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/goose-cli/src/recipes/secret_discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ mod tests {
response: None,
sub_recipes: None,
retry: None,
max_turns: None,
}
}

Expand Down Expand Up @@ -216,6 +217,7 @@ mod tests {
response: None,
sub_recipes: None,
retry: None,
max_turns: None,
};

let secrets = discover_recipe_secrets(&recipe);
Expand Down Expand Up @@ -260,6 +262,7 @@ mod tests {
response: None,
sub_recipes: None,
retry: None,
max_turns: None,
};

let secrets = discover_recipe_secrets(&recipe);
Expand Down Expand Up @@ -312,6 +315,7 @@ mod tests {
parameters: None,
response: None,
retry: None,
max_turns: None,
};

let secrets = discover_recipe_secrets(&recipe);
Expand Down
2 changes: 1 addition & 1 deletion crates/goose/src/agents/subagent_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ fn get_agent_messages(
let session_config = SessionConfig {
id: session_id.clone(),
schedule_id: None,
max_turns: task_config.max_turns.map(|v| v as u32),
max_turns: recipe.max_turns.or(task_config.max_turns.map(|v| v as u32)),
retry_config: recipe.retry,
};

Expand Down
182 changes: 115 additions & 67 deletions crates/goose/src/agents/subagent_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::agents::subagent_handler::run_complete_subagent_task;
use crate::agents::subagent_task_config::TaskConfig;
use crate::agents::tool_execution::ToolCallResult;
use crate::config::GooseMode;
use crate::prompt_template::{get_bundled_subrecipe_content, iter_bundled_subrecipes};
use crate::providers;
use crate::recipe::build_recipe::build_recipe_from_template;
use crate::recipe::local_recipes::load_local_recipe_file;
Expand Down Expand Up @@ -102,7 +103,7 @@ pub fn create_subagent_tool(sub_recipes: &[SubRecipe]) -> Tool {
)
}

fn build_tool_description(sub_recipes: &[SubRecipe]) -> String {
fn build_tool_description(user_sub_recipes: &[SubRecipe]) -> String {
let mut desc = String::from(
"Delegate a task to a subagent that runs independently with its own context.\n\n\
Modes:\n\
Expand All @@ -114,9 +115,23 @@ fn build_tool_description(sub_recipes: &[SubRecipe]) -> String {
For parallel execution, make multiple `subagent` tool calls in the same message.",
);

if !sub_recipes.is_empty() {
desc.push_str("\n\nAvailable subrecipes:");
for sr in sub_recipes {
let bundled: Vec<_> = iter_bundled_subrecipes()
.filter_map(|(name, content)| {
let recipe = Recipe::from_content(content).ok()?;
Some((name, recipe.description))
})
.collect();

if !bundled.is_empty() {
desc.push_str("\n\nBuilt-in subrecipes:");
for (name, description) in &bundled {
desc.push_str(&format!("\n• {} - {}", name, description));
}
}

if !user_sub_recipes.is_empty() {
desc.push_str("\n\nUser-defined subrecipes:");
for sr in user_sub_recipes {
let params_info = get_subrecipe_params_description(sr);
let sequential_hint = if sr.sequential_when_repeated {
" [run sequentially, not in parallel]"
Expand Down Expand Up @@ -305,40 +320,83 @@ fn build_recipe(
Ok(recipe)
}

fn params_to_values(params: &SubagentParams) -> Vec<(String, String)> {
params
.parameters
.as_ref()
.map(|p| {
p.iter()
.map(|(k, v)| {
let v = match v {
Value::String(s) => s.clone(),
other => other.to_string(),
};
(k.clone(), v)
})
.collect()
})
.unwrap_or_default()
}

fn combine_instructions(recipe: &mut Recipe, extra: Option<&str>) {
let mut combined = recipe.instructions.take().unwrap_or_default();

if let Some(prompt) = &recipe.prompt {
if !combined.is_empty() {
combined.push_str("\n\n");
}
combined.push_str(prompt);
}

if let Some(extra) = extra {
if !combined.is_empty() {
combined.push_str("\n\n");
}
combined.push_str("Additional context from parent agent:\n");
combined.push_str(extra);
}

if !combined.is_empty() {
recipe.instructions = Some(combined);
}
}

fn build_subrecipe(
subrecipe_name: &str,
params: &SubagentParams,
sub_recipes: &HashMap<String, SubRecipe>,
user_sub_recipes: &HashMap<String, SubRecipe>,
) -> Result<Recipe> {
let sub_recipe = sub_recipes.get(subrecipe_name).ok_or_else(|| {
let available: Vec<_> = sub_recipes.keys().cloned().collect();
if let Some(content) = get_bundled_subrecipe_content(subrecipe_name) {
let mut recipe = build_recipe_from_template(
content.to_string(),
&PathBuf::new(),
params_to_values(params),
None::<fn(&str, &str) -> Result<String, anyhow::Error>>,
)?;
combine_instructions(&mut recipe, params.instructions.as_deref());
return Ok(recipe);
}

let sub_recipe = user_sub_recipes.get(subrecipe_name).ok_or_else(|| {
let bundled: Vec<_> = iter_bundled_subrecipes().map(|(n, _)| n).collect();
let user: Vec<_> = user_sub_recipes.keys().map(|s| s.as_str()).collect();
anyhow!(
"Unknown subrecipe '{}'. Available: {}",
"Unknown subrecipe '{}'. Built-in: [{}]. User-defined: [{}]",
subrecipe_name,
available.join(", ")
bundled.join(", "),
user.join(", ")
)
})?;

let recipe_file = load_local_recipe_file(&sub_recipe.path)
.map_err(|e| anyhow!("Failed to load subrecipe '{}': {}", subrecipe_name, e))?;

let mut param_values: Vec<(String, String)> = Vec::new();

if let Some(values) = &sub_recipe.values {
for (k, v) in values {
param_values.push((k.clone(), v.clone()));
}
}

if let Some(provided_params) = &params.parameters {
for (k, v) in provided_params {
let value_str = match v {
Value::String(s) => s.clone(),
other => other.to_string(),
};
param_values.push((k.clone(), value_str));
}
}
let mut param_values: Vec<(String, String)> = sub_recipe
.values
.as_ref()
.map(|v| v.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
.unwrap_or_default();
param_values.extend(params_to_values(params));

let mut recipe = build_recipe_from_template(
recipe_file.content,
Expand All @@ -348,33 +406,7 @@ fn build_subrecipe(
)
.map_err(|e| anyhow!("Failed to build subrecipe: {}", e))?;

// Merge prompt into instructions so the subagent gets the actual task.
// The subagent handler uses `instructions` as the user message.
let mut combined = String::new();

if let Some(instructions) = &recipe.instructions {
combined.push_str(instructions);
}

if let Some(prompt) = &recipe.prompt {
if !combined.is_empty() {
combined.push_str("\n\n");
}
combined.push_str(prompt);
}

if let Some(extra_instructions) = &params.instructions {
if !combined.is_empty() {
combined.push_str("\n\n");
}
combined.push_str("Additional context from parent agent:\n");
combined.push_str(extra_instructions);
}

if !combined.is_empty() {
recipe.instructions = Some(combined);
}

combine_instructions(&mut recipe, params.instructions.as_deref());
Ok(recipe)
}

Expand Down Expand Up @@ -462,19 +494,18 @@ mod tests {
}

#[test]
fn test_create_tool_without_subrecipes() {
fn test_create_tool_includes_bundled_subrecipes() {
let tool = create_subagent_tool(&[]);
assert_eq!(tool.name, "subagent");
assert!(tool.description.as_ref().unwrap().contains("Ad-hoc"));
assert!(!tool
.description
.as_ref()
.unwrap()
.contains("Available subrecipes"));
let desc = tool.description.as_ref().unwrap();
assert!(desc.contains("Ad-hoc"));
assert!(desc.contains("Built-in subrecipes:"));
assert!(desc.contains("investigator"));
assert!(desc.contains("planner"));
}

#[test]
fn test_create_tool_with_subrecipes() {
fn test_create_tool_with_user_subrecipes() {
let sub_recipes = vec![SubRecipe {
name: "test_recipe".to_string(),
path: "test.yaml".to_string(),
Expand All @@ -484,12 +515,10 @@ mod tests {
}];

let tool = create_subagent_tool(&sub_recipes);
assert!(tool
.description
.as_ref()
.unwrap()
.contains("Available subrecipes"));
assert!(tool.description.as_ref().unwrap().contains("test_recipe"));
let desc = tool.description.as_ref().unwrap();
assert!(desc.contains("Built-in subrecipes:"));
assert!(desc.contains("User-defined subrecipes:"));
assert!(desc.contains("test_recipe"));
}

#[test]
Expand Down Expand Up @@ -538,4 +567,23 @@ mod tests {
assert_eq!(params.extensions, Some(vec!["developer".to_string()]));
assert!(!params.summary);
}

#[test]
fn test_build_subrecipe_bundled() {
let params = SubagentParams {
instructions: Some("Find the main entry point".to_string()),
subrecipe: Some("investigator".to_string()),
parameters: None,
extensions: None,
settings: None,
summary: true,
};

let recipe = build_subrecipe("investigator", &params, &HashMap::new()).unwrap();
assert!(recipe.instructions.is_some());
let instructions = recipe.instructions.unwrap();
assert!(instructions.contains("Investigator"));
assert!(instructions.contains("Additional context from parent agent:"));
assert!(instructions.contains("Find the main entry point"));
}
}
23 changes: 23 additions & 0 deletions crates/goose/src/prompt_template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,29 @@ pub fn render_inline_once<T: Serialize>(
Ok(rendered.trim().to_string())
}

pub fn get_bundled_subrecipe_content(name: &str) -> Option<&'static str> {
let path = format!("subrecipes/{}.yaml", name);
CORE_PROMPTS_DIR
.get_file(&path)
.and_then(|f| std::str::from_utf8(f.contents()).ok())
}

pub fn iter_bundled_subrecipes() -> impl Iterator<Item = (&'static str, &'static str)> {
CORE_PROMPTS_DIR
.get_dir("subrecipes")
.into_iter()
.flat_map(|dir| dir.files())
.filter_map(|file| {
let ext = file.path().extension().and_then(|e| e.to_str());
if ext != Some("yaml") && ext != Some("yml") {
return None;
}
let name = file.path().file_stem()?.to_str()?;
let content = std::str::from_utf8(file.contents()).ok()?;
Some((name, content))
})
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
44 changes: 44 additions & 0 deletions crates/goose/src/prompts/subrecipes/investigator.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
version: "1.0.0"
title: Investigator
description: >
Read-only research into codebases and systems. Use when you need to understand
how something works, trace bugs, or gather information before acting.

instructions: |
You are Investigator, an expert at reverse-engineering complex software systems.

Build a complete mental model: identify components, trace connections, understand
data flow, foresee consequences of changes.

READ-ONLY. Never create, modify, or delete files or system state. Observe, analyze, report.

Investigate whatever the task requires:
- Code: dependencies, call graphs, data structures, algorithms
- Configuration: env vars, config files, feature flags, secrets
- Infrastructure: containers, CI/CD, deployment pipelines, cloud resources
- Runtime: logs, metrics, error patterns, performance
- Data: schemas, migrations, data flow
- APIs: endpoints, contracts, authentication

How to work:
- Start with high-value clues, broaden as needed
- Follow imports, trace calls, check configs
- Understand full subsystems, not just the first relevant file
- Consider side effects and ripple effects
- Question why code is written the way it is
- Check git history when context matters: git log, git blame

Tools:
- developer__analyze for structure and call graphs
- developer__shell with rg for searching, git for history
- developer__text_editor for reading files

Output a clear report:
- What you found and what it means
- Key locations and their purpose
- How components connect
- Recommendations for the parent agent

If the next step is implementation, recommend consulting Planner first.

max_turns: 50
Loading
Loading