diff --git a/crates/goose/src/agents/subagent_execution_tool/mod.rs b/crates/goose/src/agents/subagent_execution_tool/mod.rs index cd21a4eb21..f7b552795e 100644 --- a/crates/goose/src/agents/subagent_execution_tool/mod.rs +++ b/crates/goose/src/agents/subagent_execution_tool/mod.rs @@ -3,6 +3,8 @@ pub mod lib; pub mod notification_events; pub mod subagent_tool; +#[cfg(test)] +mod subagent_tool_test; pub mod task_execution_tracker; pub mod task_types; pub mod tasks; diff --git a/crates/goose/src/agents/subagent_execution_tool/subagent_tool.rs b/crates/goose/src/agents/subagent_execution_tool/subagent_tool.rs index 2827e69ca4..884b03bcd1 100644 --- a/crates/goose/src/agents/subagent_execution_tool/subagent_tool.rs +++ b/crates/goose/src/agents/subagent_execution_tool/subagent_tool.rs @@ -21,6 +21,10 @@ pub const SUBAGENT_TOOL_NAME: &str = "subagent"; #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct SubagentParams { + #[serde(skip_serializing_if = "Option::is_none")] + pub task_id: Option, + + #[serde(skip_serializing_if = "Option::is_none")] pub instructions: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -106,7 +110,8 @@ pub fn create_subagent_tool() -> Tool { "Delegate a task to an autonomous subagent. Returns the result immediately. Use this tool to offload complex work (research, coding, critique) to a specialized agent. Defaults to 'return_last_only=true' for concise results. - Accepts standard recipe parameters like 'extensions', 'settings', 'retry', etc." + Accepts standard recipe parameters like 'extensions', 'settings', 'retry', etc. + Can also be used to execute an existing task by providing 'task_id' (e.g. from a sub-recipe)." .to_string(), input_schema, ) @@ -194,8 +199,10 @@ impl TryFrom for Recipe { type Error = anyhow::Error; fn try_from(params: SubagentParams) -> Result { - if params.instructions.is_none() && params.prompt.is_none() { - return Err(anyhow!("Either 'instructions' or 'prompt' is required")); + if params.task_id.is_none() && params.instructions.is_none() && params.prompt.is_none() { + return Err(anyhow!( + "Either 'instructions' or 'prompt' is required unless 'task_id' is provided" + )); } let mut builder = Recipe::builder() @@ -207,6 +214,11 @@ impl TryFrom for Recipe { .unwrap_or_else(|| "Inline recipe task".to_string()), ); + // Allow building partial recipe if we are just executing a task ID + if params.task_id.is_some() && params.instructions.is_none() && params.prompt.is_none() { + builder = builder.instructions("Executing existing task".to_string()); + } + if let Some(inst) = params.instructions { builder = builder.instructions(inst); } diff --git a/crates/goose/src/agents/subagent_execution_tool/subagent_tool_test.rs b/crates/goose/src/agents/subagent_execution_tool/subagent_tool_test.rs new file mode 100644 index 0000000000..e9dfc326ed --- /dev/null +++ b/crates/goose/src/agents/subagent_execution_tool/subagent_tool_test.rs @@ -0,0 +1,66 @@ +use crate::agents::subagent_execution_tool::subagent_tool::SubagentParams; +use crate::recipe::Recipe; + +#[test] +fn test_subagent_params_schema() { + let schema = rmcp::schemars::schema_for!(SubagentParams); + let json = serde_json::to_value(&schema).unwrap(); + let properties = json.get("properties").unwrap().as_object().unwrap(); + + // Check for task_id + assert!(properties.contains_key("task_id")); + + // Check for instructions + assert!(properties.contains_key("instructions")); +} + +#[test] +fn test_try_from_params_with_task_id() { + let params = SubagentParams { + task_id: Some("task-123".to_string()), + instructions: None, + prompt: None, + subagent_type: None, + title: None, + description: None, + version: None, + extensions: None, + settings: None, + activities: None, + author: None, + parameters: None, + response: None, + sub_recipes: None, + retry: None, + return_last_only: true, + }; + + // Should succeed because task_id is present, even without instructions + let recipe = Recipe::try_from(params).unwrap(); + assert_eq!(recipe.instructions, Some("Executing existing task".to_string())); +} + +#[test] +fn test_try_from_params_missing_instructions() { + let params = SubagentParams { + task_id: None, + instructions: None, + prompt: None, + subagent_type: None, + title: None, + description: None, + version: None, + extensions: None, + settings: None, + activities: None, + author: None, + parameters: None, + response: None, + sub_recipes: None, + retry: None, + return_last_only: true, + }; + + // Should fail because neither task_id nor instructions are present + assert!(Recipe::try_from(params).is_err()); +}