Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
75d3cd1
initial version of run sub recipe multiple times
lifeizhou-ap Jul 7, 2025
cb0e6a5
added tests, rename functions
lifeizhou-ap Jul 7, 2025
476651f
get sub recipe json output
lifeizhou-ap Jul 8, 2025
b50d245
added showing parallel running dashboard
lifeizhou-ap Jul 8, 2025
9ca6b25
better view
lifeizhou-ap Jul 8, 2025
7446692
clean up dashboard.rs
lifeizhou-ap Jul 8, 2025
b848409
fixed tests
lifeizhou-ap Jul 8, 2025
a02c056
fixed scenario that only run params are specified
lifeizhou-ap Jul 8, 2025
f7832da
display param values in the task list
lifeizhou-ap Jul 8, 2025
e601ca2
use stream to send the output to session
lifeizhou-ap Jul 8, 2025
4d11f37
refactored the files
lifeizhou-ap Jul 8, 2025
d8fdedd
better console ui and run sub recipe in no-session mode
lifeizhou-ap Jul 8, 2025
f1c38f9
added timeout in task execution
lifeizhou-ap Jul 8, 2025
54d5224
cleaned up some utils and added the error tool response
lifeizhou-ap Jul 9, 2025
47e769e
return task output if task times out
lifeizhou-ap Jul 9, 2025
85d5286
changed console output a bit
lifeizhou-ap Jul 9, 2025
2ccfb68
test fmt
lifeizhou-ap Jul 9, 2025
4453998
refactored the code
lifeizhou-ap Jul 9, 2025
c984764
Merge branch 'main' into lifei/run-sub-recipe-multiple-times
lifeizhou-ap Jul 9, 2025
c14da81
refactored parsing recipe
lifeizhou-ap Jul 9, 2025
a01d712
created taskExecutionNotificationEvent
lifeizhou-ap Jul 9, 2025
41c46a6
added tests
lifeizhou-ap Jul 9, 2025
2c0f24c
revert some unexpected deletion
lifeizhou-ap Jul 9, 2025
41fc7ca
removed initial worker size
lifeizhou-ap Jul 9, 2025
353c52a
removed worker config
lifeizhou-ap Jul 9, 2025
17a5213
fixed fmt
lifeizhou-ap Jul 9, 2025
4f52ab1
renamed types.rs to task_types.rs
lifeizhou-ap Jul 11, 2025
8f4f55f
dynamic tool
Jul 14, 2025
6c6113d
add dynamic task tool
Jul 14, 2025
2c0d73f
draft
Jul 14, 2025
0e01630
feat: dynamic tasks (#3414)
Jul 15, 2025
5007699
task config
Jul 15, 2025
1f010d1
pass extension manager to TaskConfig, remove list_tools to avoid lock…
Jul 15, 2025
dc0ec41
control output
Jul 15, 2025
0c513b4
mute notifications'
Jul 15, 2025
f65ba7b
Merge branch 'lifei/run-sub-recipe-multiple-times' into wtang/execute…
Jul 15, 2025
40546b8
Merge branch 'main' into wtang/execute_dynamic_tasks
Jul 16, 2025
545ea23
merge
Jul 16, 2025
ae0933e
stdout
Jul 16, 2025
8d7e402
rm probablistic test
Jul 16, 2025
564369a
fmt
Jul 16, 2025
b28ce9c
rm test
Jul 16, 2025
7c492e3
Merge branch 'main' into wtang/execute_dynamic_tasks
lifeizhou-ap Jul 17, 2025
11f33ff
fixed merge conflicts
lifeizhou-ap Jul 17, 2025
e4d822a
fixed fmt and clippy
lifeizhou-ap Jul 17, 2025
aa25ef4
more fix on merge conflicts
lifeizhou-ap Jul 17, 2025
161c809
fix test compilation
lifeizhou-ap Jul 17, 2025
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
1 change: 1 addition & 0 deletions crates/goose-cli/src/session/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ 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;
}
Expand Down
9 changes: 5 additions & 4 deletions crates/goose-cli/src/session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ mod prompt;
mod task_execution_display;
mod thinking;

use crate::session::task_execution_display::TASK_EXECUTION_NOTIFICATION_TYPE;
use crate::session::task_execution_display::{
format_task_execution_notification, TASK_EXECUTION_NOTIFICATION_TYPE,
};
use std::io::Write;

pub use self::export::message_to_markdown;
pub use builder::{build_session, SessionBuilderConfig, SessionSettings};
Expand All @@ -20,8 +23,6 @@ use goose::permission::PermissionConfirmation;
use goose::providers::base::Provider;
pub use goose::session::Identifier;
use goose::utils::safe_truncate;
use std::io::Write;
use task_execution_display::format_task_execution_notification;

use anyhow::{Context, Result};
use completion::GooseCompleter;
Expand Down Expand Up @@ -1077,7 +1078,7 @@ impl Session {

// Handle subagent notifications - show immediately
if let Some(_id) = subagent_id {
// Show subagent notifications immediately (no buffering) with compact spacing
// TODO: proper display for subagent notifications
if interactive {
let _ = progress_bars.hide();
println!("{}", console::style(&formatted_message).green().dim());
Expand Down
22 changes: 20 additions & 2 deletions crates/goose-cli/src/session/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -463,8 +463,26 @@ fn print_params(value: &Value, depth: usize, debug: bool) {
}
}
Value::String(s) => {
if !debug && s.len() > get_tool_params_max_length() {
println!("{}{}: {}", indent, style(key).dim(), style("...").dim());
// Special handling for text_instruction to show more content
let max_length = if key == "text_instruction" {
200 // Allow longer display for text instructions
} else {
get_tool_params_max_length()
};

if !debug && s.len() > max_length {
// For text instructions, show a preview instead of just "..."
if key == "text_instruction" {
let preview = &s[..max_length.saturating_sub(3)];
println!(
"{}{}: {}",
indent,
style(key).dim(),
style(format!("{}...", preview)).green()
);
} else {
println!("{}{}: {}", indent, style(key).dim(), style("...").dim());
}
} else {
println!("{}{}: {}", indent, style(key).dim(), style(s).green());
}
Expand Down
4 changes: 2 additions & 2 deletions crates/goose-cli/src/session/task_execution_display/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use goose::agents::sub_recipe_execution_tool::lib::TaskStatus;
use goose::agents::sub_recipe_execution_tool::notification_events::{
use goose::agents::subagent_execution_tool::lib::TaskStatus;
use goose::agents::subagent_execution_tool::notification_events::{
TaskExecutionNotificationEvent, TaskInfo,
};
use serde_json::Value;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::*;
use goose::agents::sub_recipe_execution_tool::notification_events::{
use goose::agents::subagent_execution_tool::notification_events::{
FailedTaskInfo, TaskCompletionStats, TaskExecutionStats,
};
use serde_json::json;
Expand Down
58 changes: 24 additions & 34 deletions crates/goose/src/agents/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ 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::sub_recipe_execution_tool::sub_recipe_execute_task_tool::{
self, SUB_RECIPE_EXECUTE_TASK_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::tasks_manager::TasksManager;
use crate::agents::sub_recipe_manager::SubRecipeManager;
use crate::agents::subagent_execution_tool::subagent_execute_task_tool::{
self, SUBAGENT_EXECUTE_TASK_TOOL_NAME,
};
use crate::agents::subagent_execution_tool::tasks_manager::TasksManager;
use crate::config::{Config, ExtensionConfigManager, PermissionManager};
use crate::message::{push_message, Message};
use crate::permission::permission_judge::check_tool_permissions;
Expand Down Expand Up @@ -48,21 +51,18 @@ use mcp_core::{
prompt::Prompt, protocol::GetPromptResult, tool::Tool, Content, ToolError, ToolResult,
};

use crate::agents::subagent_tools::SUBAGENT_RUN_TASK_TOOL_NAME;

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};
use crate::agents::subagent_task_config::TaskConfig;

const DEFAULT_MAX_TURNS: u32 = 1000;

/// The main goose Agent
pub struct Agent {
pub(super) provider: Mutex<Option<Arc<dyn Provider>>>,
pub(super) extension_manager: RwLock<ExtensionManager>,
pub(super) extension_manager: Arc<RwLock<ExtensionManager>>,
pub(super) sub_recipe_manager: Mutex<SubRecipeManager>,
pub(super) tasks_manager: TasksManager,
pub(super) final_output_tool: Mutex<Option<FinalOutputTool>>,
Expand All @@ -76,7 +76,7 @@ pub struct Agent {
pub(super) tool_monitor: Mutex<Option<ToolMonitor>>,
pub(super) router_tool_selector: Mutex<Option<Arc<Box<dyn RouterToolSelector>>>>,
pub(super) scheduler_service: Mutex<Option<Arc<dyn SchedulerTrait>>>,
pub(super) subagent_manager: Mutex<Option<SubAgentManager>>,
pub(super) mcp_tx: Mutex<mpsc::Sender<JsonRpcMessage>>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

If I understand correctly, this will be eventually passed to the subagent via the subagent_execute_task_tool

I feel we can reuse the channel here https://github.com/block/goose/blob/wtang/execute_dynamic_tasks/crates/goose/src/agents/subagent_execution_tool/subagent_execute_task_tool.rs#L67 instead

Copy link
Author

@wendytang wendytang Jul 17, 2025

Choose a reason for hiding this comment

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

we can address the channel updates as a follow up!

pub(super) mcp_notification_rx: Arc<Mutex<mpsc::Receiver<JsonRpcMessage>>>,
}

Expand Down Expand Up @@ -137,7 +137,7 @@ impl Agent {

Self {
provider: Mutex::new(None),
extension_manager: RwLock::new(ExtensionManager::new()),
extension_manager: Arc::new(RwLock::new(ExtensionManager::new())),
sub_recipe_manager: Mutex::new(SubRecipeManager::new()),
tasks_manager: TasksManager::new(),
final_output_tool: Mutex::new(None),
Expand All @@ -152,7 +152,7 @@ impl Agent {
router_tool_selector: Mutex::new(None),
scheduler_service: Mutex::new(None),
// Initialize with MCP notification support
subagent_manager: Mutex::new(Some(SubAgentManager::new(mcp_tx))),
mcp_tx: Mutex::new(mcp_tx),
mcp_notification_rx: Arc::new(Mutex::new(mcp_rx)),
}
}
Expand Down Expand Up @@ -300,12 +300,20 @@ impl Agent {
&self.tasks_manager,
)
.await
} else if tool_call.name == SUB_RECIPE_EXECUTE_TASK_TOOL_NAME {
sub_recipe_execute_task_tool::run_tasks(
} else if tool_call.name == SUBAGENT_EXECUTE_TASK_TOOL_NAME {
let provider = self.provider().await.ok();
let mcp_tx = self.mcp_tx.lock().await.clone();

let task_config =
TaskConfig::new(provider, Some(Arc::clone(&self.extension_manager)), mcp_tx);
Copy link
Collaborator

Choose a reason for hiding this comment

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

  • we can add provider as default_provider and extension_manager in the tasks_manager so that we don't need to pass the task_config when run_tasks.

  • mcp_tx probably does not need to pass as we can reuse the one inside subagent_execute_task_tool

  • Are we going to pass the extensions instead of extension_manager later?

Copy link
Author

Choose a reason for hiding this comment

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

Are we going to pass the extensions instead of extension_manager later?

No turns out we need the extension manager in order to dispatch mcp tool calls

Copy link
Author

Choose a reason for hiding this comment

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

mcp_tx probably does not need to pass as we can reuse the one inside subagent_execute_task_tool

I agree, we can address this afterwards.

Copy link
Collaborator

@lifeizhou-ap lifeizhou-ap Jul 17, 2025

Choose a reason for hiding this comment

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

Are we going to pass the extensions instead of extension_manager later?

No turns out we need the extension manager in order to dispatch mcp tool calls

Just curious how sub-recipe pass the extensions to the subAgent.rs then? I remember we were planning to have extensions as an attribute of subAgent (eg SubAgent constructor params)

subagent_execute_task_tool::run_tasks(
tool_call.arguments.clone(),
task_config,
&self.tasks_manager,
)
.await
} else if tool_call.name == DYNAMIC_TASK_TOOL_NAME_PREFIX {
create_dynamic_task(tool_call.arguments.clone(), &self.tasks_manager).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 All @@ -321,11 +329,6 @@ impl Agent {
)
} else if tool_call.name == PLATFORM_SEARCH_AVAILABLE_EXTENSIONS_TOOL_NAME {
ToolCallResult::from(extension_manager.search_available_extensions().await)
} else if tool_call.name == SUBAGENT_RUN_TASK_TOOL_NAME {
ToolCallResult::from(
self.handle_run_subagent_task(tool_call.arguments.clone())
.await,
)
} else if self.is_frontend_tool(&tool_call.name).await {
// For frontend tools, return an error indicating we need frontend execution
ToolCallResult::from(Err(ToolError::ExecutionError(
Expand Down Expand Up @@ -567,11 +570,8 @@ impl Agent {
platform_tools::manage_schedule_tool(),
]);

// 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());
}
// Dynamic task tool
prefixed_tools.push(create_dynamic_task_tool());

// Add resource tools if supported
if extension_manager.supports_resources() {
Expand All @@ -589,8 +589,7 @@ impl Agent {
if let Some(final_output_tool) = self.final_output_tool.lock().await.as_ref() {
prefixed_tools.push(final_output_tool.tool());
}
prefixed_tools
.push(sub_recipe_execute_task_tool::create_sub_recipe_execute_task_tool());
prefixed_tools.push(subagent_execute_task_tool::create_subagent_execute_task_tool());
}

prefixed_tools
Expand Down Expand Up @@ -1074,15 +1073,6 @@ impl Agent {
let mut current_provider = self.provider.lock().await;
*current_provider = Some(provider.clone());

// Initialize subagent manager with MCP notification support
// Need to recreate the MCP channel since we're replacing the manager
let (mcp_tx, mcp_rx) = mpsc::channel(100);
{
let mut rx_guard = self.mcp_notification_rx.lock().await;
*rx_guard = mcp_rx;
}
*self.subagent_manager.lock().await = Some(SubAgentManager::new(mcp_tx));

self.update_router_tool_selector(Some(provider), None)
.await?;
Ok(())
Expand Down
11 changes: 4 additions & 7 deletions crates/goose/src/agents/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,11 @@ mod reply_parts;
mod router_tool_selector;
mod router_tools;
mod schedule_tool;
pub mod sub_recipe_execution_tool;
pub mod sub_recipe_manager;
pub mod subagent;
pub mod subagent_execution_tool;
pub mod subagent_handler;
pub mod subagent_manager;
pub mod subagent_tools;
pub mod subagent_types;
mod subagent_task_config;
mod tool_execution;
mod tool_router_index_manager;
pub(crate) mod tool_vectordb;
Expand All @@ -27,7 +25,6 @@ pub use agent::{Agent, AgentEvent};
pub use extension::ExtensionConfig;
pub use extension_manager::ExtensionManager;
pub use prompt_manager::PromptManager;
pub use subagent::{SubAgent, SubAgentConfig, SubAgentProgress, SubAgentStatus};
pub use subagent_manager::SubAgentManager;
pub use subagent_types::SpawnSubAgentArgs;
pub use subagent::{SubAgent, SubAgentProgress, SubAgentStatus};
pub use subagent_task_config::TaskConfig;
pub use types::{FrontendTool, SessionConfig};
147 changes: 147 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,147 @@
// =======================================
// Module: Dynamic Task Tools
// Handles creation of tasks dynamically without sub-recipes
// =======================================
use crate::agents::subagent_execution_tool::tasks_manager::TasksManager;
use crate::agents::subagent_execution_tool::{lib::ExecutionMode, task_types::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(
DYNAMIC_TASK_TOOL_NAME_PREFIX.to_string(),
"Use this tool to create one or more dynamic tasks from a shared text instruction and varying parameters.\
How it works:
- Provide a single text instruction
- Use the 'task_parameters' field to pass an array of parameter sets
- Each resulting task will use the same instruction with different parameter values
This is useful when performing the same operation across many inputs (e.g., getting weather for multiple cities, searching multiple slack channels, iterating through various linear tickets, etc).
Once created, these tasks should be passed to the 'subagent__execute_task' tool for execution. Tasks can run sequentially or in parallel.
---
What is a 'subagent'?
A 'subagent' is a stateless sub-process that executes a single task independently. Use subagents when:
- You want to parallelize similar work across different inputs
- You are not sure your search or operation will succeed on the first try
Each subagent receives a task with a defined payload and returns a result, which is not visible to the user unless explicitly summarized by the system.
---
Examples of 'task_parameters' for a single task:
text_instruction: Search for the config file in the root directory.
Examples of 'task_parameters' for multiple tasks:
text_instruction: Get weather for Melbourne.
timeout_seconds: 300
text_instruction: Get weather for Los Angeles.
timeout_seconds: 300
text_instruction: Get weather for San Francisco.
timeout_seconds: 300
".to_string(),
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("Dynamic Task Creation".to_string()),
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 payload = json!({
"text_instruction": text_instruction
});

Task {
id: uuid::Uuid::new_v4().to_string(),
task_type: "text_instruction".to_string(),
payload,
}
})
.collect()
}

fn create_task_execution_payload(tasks: Vec<Task>, execution_mode: ExecutionMode) -> Value {
let task_ids: Vec<String> = tasks.iter().map(|task| task.id.clone()).collect();
json!({
"task_ids": task_ids,
"execution_mode": execution_mode
})
}

pub async fn create_dynamic_task(params: Value, tasks_manager: &TasksManager) -> 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 {
ExecutionMode::Parallel
} else {
ExecutionMode::Sequential
};

let task_execution_payload = create_task_execution_payload(tasks.clone(), 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
))))
}
};
tasks_manager.save_tasks(tasks.clone()).await;
ToolCallResult::from(Ok(vec![Content::text(tasks_json)]))
}
Loading
Loading