Skip to content
Merged
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
2 changes: 2 additions & 0 deletions crates/goose-cli/src/session/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
8 changes: 6 additions & 2 deletions crates/goose/src/agents/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -559,7 +563,7 @@ impl Agent {
// Add subagent tool (only if ALPHA_FEATURES is enabled)
let config = Config::global();
if config.get_param::<bool>("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
Expand Down
138 changes: 138 additions & 0 deletions crates/goose/src/agents/recipe_tools/dynamic_task_tools.rs
Original file line number Diff line number Diff line change
@@ -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<Value> {
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> {
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<Task>, 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(&params);

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)]))
}
1 change: 1 addition & 0 deletions crates/goose/src/agents/recipe_tools/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod dynamic_task_tools;
pub mod param_utils;
pub mod sub_recipe_tools;
4 changes: 2 additions & 2 deletions crates/goose/src/agents/recipe_tools/sub_recipe_tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down