Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
15 changes: 13 additions & 2 deletions crates/goose-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,7 @@ pub async fn cli() -> Result<()> {
debug,
max_tool_repetitions,
interactive: true, // Session command is always interactive
sub_recipes: None,
})
.await;
setup_logging(
Expand Down Expand Up @@ -689,8 +690,12 @@ pub async fn cli() -> Result<()> {
params,
explain,
}) => {
let (input_config, session_settings) = match (instructions, input_text, recipe, explain)
{
let (input_config, session_settings, sub_recipes) = match (
instructions,
input_text,
recipe,
explain,
) {
(Some(file), _, _, _) if file == "-" => {
let mut input = String::new();
std::io::stdin()
Expand All @@ -704,6 +709,7 @@ pub async fn cli() -> Result<()> {
additional_system_prompt: system,
},
None,
None,
)
}
(Some(file), _, _, _) => {
Expand All @@ -721,6 +727,7 @@ pub async fn cli() -> Result<()> {
additional_system_prompt: None,
},
None,
None,
)
}
(_, Some(text), _, _) => (
Expand All @@ -730,6 +737,7 @@ pub async fn cli() -> Result<()> {
additional_system_prompt: system,
},
None,
None,
),
(_, _, Some(recipe_name), explain) => {
if explain {
Expand All @@ -752,6 +760,7 @@ pub async fn cli() -> Result<()> {
goose_model: s.goose_model,
temperature: s.temperature,
}),
recipe.sub_recipes,
)
}
(None, None, None, _) => {
Expand All @@ -773,6 +782,7 @@ pub async fn cli() -> Result<()> {
debug,
max_tool_repetitions,
interactive, // Use the interactive flag from the Run command
sub_recipes,
})
.await;

Expand Down Expand Up @@ -889,6 +899,7 @@ pub async fn cli() -> Result<()> {
debug: false,
max_tool_repetitions: None,
interactive: true, // Default case is always interactive
sub_recipes: None,
})
.await;
setup_logging(
Expand Down
1 change: 1 addition & 0 deletions crates/goose-cli/src/commands/bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub async fn agent_generator(
debug: false,
max_tool_repetitions: None,
interactive: false, // Benchmarking is non-interactive
sub_recipes: None,
})
.await;

Expand Down
18 changes: 2 additions & 16 deletions crates/goose-cli/src/recipes/recipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ use crate::recipes::print_recipe::{
use crate::recipes::search_recipe::retrieve_recipe_file;
use goose::recipe::{Recipe, RecipeParameter, RecipeParameterRequirement};
use minijinja::{Environment, Error, Template, UndefinedBehavior};
use serde_json::Value as JsonValue;
use serde_yaml::Value as YamlValue;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;

Expand Down Expand Up @@ -47,7 +45,7 @@ pub fn load_recipe_as_template(recipe_name: &str, params: Vec<(String, String)>)

let rendered_content = render_content_with_params(&recipe_file_content, &params_for_template)?;

let recipe = parse_recipe_content(&rendered_content)?;
let recipe = Recipe::from_content(&rendered_content)?;

// Display information about the loaded recipe
println!(
Expand Down Expand Up @@ -106,7 +104,7 @@ pub fn explain_recipe_with_parameters(
}

fn validate_recipe_file_parameters(recipe_file_content: &str) -> Result<Recipe> {
let recipe_from_recipe_file: Recipe = parse_recipe_content(recipe_file_content)?;
let recipe_from_recipe_file: Recipe = Recipe::from_content(recipe_file_content)?;
validate_optional_parameters(&recipe_from_recipe_file)?;
validate_parameters_in_template(&recipe_from_recipe_file.parameters, recipe_file_content)?;
Ok(recipe_from_recipe_file)
Expand Down Expand Up @@ -183,18 +181,6 @@ fn validate_optional_parameters(recipe: &Recipe) -> Result<()> {
}
}

fn parse_recipe_content(content: &str) -> Result<Recipe> {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to goose package to be reusable

if serde_json::from_str::<JsonValue>(content).is_ok() {
Ok(serde_json::from_str(content)?)
} else if serde_yaml::from_str::<YamlValue>(content).is_ok() {
Ok(serde_yaml::from_str(content)?)
} else {
Err(anyhow::anyhow!(
"Unsupported file format for recipe file. Expected .yaml or .json"
))
}
}

fn extract_template_variables(template_str: &str) -> Result<HashSet<String>> {
let mut env = Environment::new();
env.set_undefined_behavior(UndefinedBehavior::Strict);
Expand Down
9 changes: 8 additions & 1 deletion crates/goose-cli/src/session/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use goose::agents::extension::ExtensionError;
use goose::agents::Agent;
use goose::config::{Config, ExtensionConfig, ExtensionConfigManager};
use goose::providers::create;
use goose::recipe::SubRecipe;
use goose::session;
use goose::session::Identifier;
use mcp_client::transport::Error as McpClientError;
Expand Down Expand Up @@ -42,6 +43,8 @@ pub struct SessionBuilderConfig {
pub max_tool_repetitions: Option<u32>,
/// Whether this session will be used interactively (affects debugging prompts)
pub interactive: bool,
/// Sub-recipes to add to the session
pub sub_recipes: Option<Vec<SubRecipe>>,
}

/// Offers to help debug an extension failure by creating a minimal debugging session
Expand Down Expand Up @@ -170,6 +173,9 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> Session {

// Create the agent
let agent: Agent = Agent::new();
if let Some(sub_recipes) = session_config.sub_recipes {
agent.add_sub_recipes(sub_recipes).await;
}
let new_provider = match create(&provider_name, model_config) {
Ok(provider) => provider,
Err(e) => {
Expand Down Expand Up @@ -212,7 +218,7 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> Session {
}

// Handle session file resolution and resuming
let session_file = if session_config.no_session {
let session_file: std::path::PathBuf = if session_config.no_session {
// Use a temporary path that won't be written to
#[cfg(unix)]
{
Expand Down Expand Up @@ -486,6 +492,7 @@ mod tests {
debug: true,
max_tool_repetitions: Some(5),
interactive: true,
sub_recipes: None,
};

assert_eq!(config.extensions.len(), 1);
Expand Down
37 changes: 31 additions & 6 deletions crates/goose/src/agents/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ use futures_util::stream;
use futures_util::stream::StreamExt;
use mcp_core::protocol::JsonRpcMessage;

use crate::agents::sub_recipe_manager::SubRecipeManager;
use crate::config::{Config, ExtensionConfigManager, PermissionManager};
use crate::message::Message;
use crate::permission::permission_judge::check_tool_permissions;
use crate::permission::PermissionConfirmation;
use crate::providers::base::Provider;
use crate::providers::errors::ProviderError;
use crate::recipe::{Author, Recipe, Settings};
use crate::recipe::{Author, Recipe, Settings, SubRecipe};
use crate::tool_monitor::{ToolCall, ToolMonitor};
use regex::Regex;
use serde_json::Value;
Expand Down Expand Up @@ -50,6 +51,7 @@ use super::tool_execution::{ToolCallResult, CHAT_MODE_TOOL_SKIPPED_RESPONSE, DEC
pub struct Agent {
pub(super) provider: Mutex<Option<Arc<dyn Provider>>>,
pub(super) extension_manager: Mutex<ExtensionManager>,
pub(super) sub_recipe_manager: Mutex<SubRecipeManager>,

@lifeizhou-ap lifeizhou-ap Jun 18, 2025

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created a sub_recipe_manager to manage sub recipe tools and trigger the sub recipe tool call.

I could also use the extension manager to add these tool, but the extension manager is mainly for MCP. So I create a sub_recipe_manager, maybe later it could evolve to sub_agent_manager

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems great. They are distinct from extensions so I think that makes sense. It also leaves the door open to agents treating sub-recipes differently: instead of shelling out to a new goose, a future version may use the same agent or a new in-process instance

pub(super) frontend_tools: Mutex<HashMap<String, FrontendTool>>,
pub(super) frontend_instructions: Mutex<Option<String>>,
pub(super) prompt_manager: Mutex<PromptManager>,
Expand All @@ -76,6 +78,7 @@ impl Agent {
Self {
provider: Mutex::new(None),
extension_manager: Mutex::new(ExtensionManager::new()),
sub_recipe_manager: Mutex::new(SubRecipeManager::new()),
frontend_tools: Mutex::new(HashMap::new()),
frontend_instructions: Mutex::new(None),
prompt_manager: Mutex::new(PromptManager::new()),
Expand Down Expand Up @@ -182,6 +185,11 @@ impl Agent {
Ok(tools)
}

pub async fn add_sub_recipes(&self, sub_recipes: Vec<SubRecipe>) {
let mut sub_recipe_manager = self.sub_recipe_manager.lock().await;
sub_recipe_manager.add_sub_recipe_tools(sub_recipes);
}

/// Dispatch a single tool call to the appropriate client
#[instrument(skip(self, tool_call, request_id), fields(input, output))]
pub(super) async fn dispatch_tool_call(
Expand Down Expand Up @@ -224,7 +232,15 @@ impl Agent {
}

let extension_manager = self.extension_manager.lock().await;
let result: ToolCallResult = if tool_call.name == PLATFORM_READ_RESOURCE_TOOL_NAME {
let sub_recipe_manager = self.sub_recipe_manager.lock().await;

let result: ToolCallResult = if sub_recipe_manager.is_sub_recipe_tool(&tool_call.name) {
ToolCallResult::from(
sub_recipe_manager
.run_sub_recipe(&tool_call.name, tool_call.arguments.clone())
.await,
)
} else if tool_call.name == PLATFORM_READ_RESOURCE_TOOL_NAME {
// Check if the tool is read_resource and handle it separately
ToolCallResult::from(
extension_manager
Expand Down Expand Up @@ -450,16 +466,25 @@ impl Agent {

if extension_name.is_none() || extension_name.as_deref() == Some("platform") {
// Add platform tools
prefixed_tools.push(platform_tools::search_available_extensions_tool());
prefixed_tools.push(platform_tools::manage_extensions_tool());
prefixed_tools.extend([
platform_tools::search_available_extensions_tool(),
platform_tools::manage_extensions_tool(),
]);

// Add resource tools if supported
if extension_manager.supports_resources() {
prefixed_tools.push(platform_tools::read_resource_tool());
prefixed_tools.push(platform_tools::list_resources_tool());
prefixed_tools.extend([
platform_tools::read_resource_tool(),
platform_tools::list_resources_tool(),
]);
}
}

if extension_name.is_none() {
let sub_recipe_manager = self.sub_recipe_manager.lock().await;
prefixed_tools.extend(sub_recipe_manager.sub_recipe_tools.values().cloned());
}

prefixed_tools
}

Expand Down
2 changes: 2 additions & 0 deletions crates/goose/src/agents/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ pub mod extension_manager;
mod large_response_handler;
pub mod platform_tools;
pub mod prompt_manager;
mod recipe_tools;
mod reply_parts;
mod router_tool_selector;
mod router_tools;
pub mod sub_recipe_manager;
mod tool_execution;
mod tool_router_index_manager;
pub(crate) mod tool_vectordb;
Expand Down
1 change: 1 addition & 0 deletions crates/goose/src/agents/recipe_tools/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod sub_recipe_tools;
Loading