diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs index b5d3da1d7f45..0377a36e905e 100644 --- a/crates/goose-cli/src/session/builder.rs +++ b/crates/goose-cli/src/session/builder.rs @@ -187,6 +187,8 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> Session { // Create the agent let agent: Agent = Agent::new(); + + // Sub-recipes if let Some(sub_recipes) = session_config.sub_recipes { agent.add_sub_recipes(sub_recipes).await; } diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index 38a488a9781a..b9592bff808a 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -9,6 +9,9 @@ use futures::{stream, FutureExt, Stream, StreamExt, TryStreamExt}; use mcp_core::protocol::JsonRpcMessage; use crate::agents::final_output_tool::{FINAL_OUTPUT_CONTINUATION_MESSAGE, FINAL_OUTPUT_TOOL_NAME}; +use crate::agents::recipe_tools::dynamic_task_tools::{ + create_dynamic_task, create_dynamic_task_tool, DYNAMIC_TASK_TOOL_NAME_PREFIX, +}; use crate::agents::sub_recipe_execution_tool::sub_recipe_execute_task_tool::{ self, SUB_RECIPE_EXECUTE_TASK_TOOL_NAME, }; @@ -53,7 +56,6 @@ use super::final_output_tool::FinalOutputTool; use super::platform_tools; use super::router_tools; use super::subagent_manager::SubAgentManager; -use super::subagent_tools; use super::tool_execution::{ToolCallResult, CHAT_MODE_TOOL_SKIPPED_RESPONSE, DECLINED_RESPONSE}; const DEFAULT_MAX_TURNS: u32 = 1000; @@ -295,6 +297,8 @@ impl Agent { .await } else if tool_call.name == SUB_RECIPE_EXECUTE_TASK_TOOL_NAME { sub_recipe_execute_task_tool::run_tasks(tool_call.arguments.clone()).await + } else if tool_call.name == DYNAMIC_TASK_TOOL_NAME_PREFIX { + create_dynamic_task(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( @@ -559,7 +563,7 @@ impl Agent { // Add subagent tool (only if ALPHA_FEATURES is enabled) let config = Config::global(); if config.get_param::("ALPHA_FEATURES").unwrap_or(false) { - prefixed_tools.push(subagent_tools::run_task_subagent_tool()); + prefixed_tools.push(create_dynamic_task_tool()); } // Add resource tools if supported diff --git a/crates/goose/src/agents/recipe_tools/dynamic_task_tools.rs b/crates/goose/src/agents/recipe_tools/dynamic_task_tools.rs new file mode 100644 index 000000000000..77a66bee3fac --- /dev/null +++ b/crates/goose/src/agents/recipe_tools/dynamic_task_tools.rs @@ -0,0 +1,138 @@ +// ======================================= +// Module: Dynamic Task Tools +// Handles creation of tasks dynamically without sub-recipes +// ======================================= +use crate::agents::recipe_tools::sub_recipe_tools::{ + EXECUTION_MODE_PARALLEL, EXECUTION_MODE_SEQUENTIAL, +}; +use crate::agents::sub_recipe_execution_tool::lib::Task; +use crate::agents::tool_execution::ToolCallResult; +use mcp_core::{tool::ToolAnnotations, Content, Tool, ToolError}; +use serde_json::{json, Value}; + +pub const DYNAMIC_TASK_TOOL_NAME_PREFIX: &str = "dynamic_task__create_task"; + +pub fn create_dynamic_task_tool() -> Tool { + Tool::new( + format!("{}", DYNAMIC_TASK_TOOL_NAME_PREFIX), + format!( + "Creates a dynamic task object(s) based on textual instructions. \ + Provide an array of parameter sets in the 'task_parameters' field:\n\ + - For a single task: provide an array with one parameter set\n\ + - For multiple tasks: provide an array with multiple parameter sets, each with different values\n\n\ + Each task will run the same text instruction but with different parameter values. \ + This is useful when you need to execute the same instruction multiple times with varying inputs. \ + After creating the task list, pass it to the task executor to run all tasks." + ), + json!({ + "type": "object", + "properties": { + "task_parameters": { + "type": "array", + "description": "Array of parameter sets for creating tasks. \ + For a single task, provide an array with one element. \ + For multiple tasks, provide an array with multiple elements, each with different parameter values. \ + If there is no parameter set, provide an empty array.", + "items": { + "type": "object", + "properties": { + "text_instruction": { + "type": "string", + "description": "The text instruction to execute" + }, + "timeout_seconds": { + "type": "integer", + "description": "Optional timeout for the task in seconds (default: 300)", + "minimum": 1 + } + }, + "required": ["text_instruction"] + } + } + } + }), + Some(ToolAnnotations { + title: Some(format!("Dynamic Task Creation")), + read_only_hint: false, + destructive_hint: true, + idempotent_hint: false, + open_world_hint: true, + }), + ) +} + +fn extract_task_parameters(params: &Value) -> Vec { + params + .get("task_parameters") + .and_then(|v| v.as_array()) + .cloned() + .unwrap_or_default() +} + +fn create_text_instruction_tasks_from_params(task_params: &[Value]) -> Vec { + task_params + .iter() + .map(|task_param| { + let text_instruction = task_param + .get("text_instruction") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + + let timeout_seconds = task_param + .get("timeout_seconds") + .and_then(|v| v.as_u64()) + .unwrap_or(300); + + let payload = json!({ + "text_instruction": text_instruction + }); + + Task { + id: uuid::Uuid::new_v4().to_string(), + task_type: "text_instruction".to_string(), + timeout_in_seconds: Some(timeout_seconds), + payload, + } + }) + .collect() +} + +fn create_task_execution_payload(tasks: Vec, execution_mode: &str) -> Value { + json!({ + "tasks": tasks, + "execution_mode": execution_mode + }) +} + +pub async fn create_dynamic_task(params: Value) -> ToolCallResult { + let task_params_array = extract_task_parameters(¶ms); + + if task_params_array.is_empty() { + return ToolCallResult::from(Err(ToolError::ExecutionError( + "No task parameters provided".to_string(), + ))); + } + + let tasks = create_text_instruction_tasks_from_params(&task_params_array); + + // Use parallel execution if there are multiple tasks, sequential for single task + let execution_mode = if tasks.len() > 1 { + EXECUTION_MODE_PARALLEL + } else { + EXECUTION_MODE_SEQUENTIAL + }; + + let task_execution_payload = create_task_execution_payload(tasks, execution_mode); + + let tasks_json = match serde_json::to_string(&task_execution_payload) { + Ok(json) => json, + Err(e) => { + return ToolCallResult::from(Err(ToolError::ExecutionError(format!( + "Failed to serialize task list: {}", + e + )))) + } + }; + ToolCallResult::from(Ok(vec![Content::text(tasks_json)])) +} diff --git a/crates/goose/src/agents/recipe_tools/mod.rs b/crates/goose/src/agents/recipe_tools/mod.rs index 90603c88488e..6e6f28a80310 100644 --- a/crates/goose/src/agents/recipe_tools/mod.rs +++ b/crates/goose/src/agents/recipe_tools/mod.rs @@ -1,2 +1,3 @@ +pub mod dynamic_task_tools; pub mod param_utils; pub mod sub_recipe_tools; diff --git a/crates/goose/src/agents/recipe_tools/sub_recipe_tools.rs b/crates/goose/src/agents/recipe_tools/sub_recipe_tools.rs index fd0edaeaecd1..bc0347dfd509 100644 --- a/crates/goose/src/agents/recipe_tools/sub_recipe_tools.rs +++ b/crates/goose/src/agents/recipe_tools/sub_recipe_tools.rs @@ -11,8 +11,8 @@ use crate::recipe::{Recipe, RecipeParameter, RecipeParameterRequirement, SubReci use super::param_utils::prepare_command_params; pub const SUB_RECIPE_TASK_TOOL_NAME_PREFIX: &str = "subrecipe__create_task"; -const EXECUTION_MODE_PARALLEL: &str = "parallel"; -const EXECUTION_MODE_SEQUENTIAL: &str = "sequential"; +pub const EXECUTION_MODE_PARALLEL: &str = "parallel"; +pub const EXECUTION_MODE_SEQUENTIAL: &str = "sequential"; pub fn create_sub_recipe_task_tool(sub_recipe: &SubRecipe) -> Tool { let input_schema = get_input_schema(sub_recipe).unwrap();