Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
29f72b4
feat: minimal SubRecipe overhaul - convert to in-process execution
tlongwell-block Oct 8, 2025
26057d2
cargo fmt
tlongwell-block Oct 8, 2025
f20425a
initial work on tasktype removal
tlongwell-block Oct 9, 2025
f03fc85
testing deletions and fixes
tlongwell-block Oct 9, 2025
b3f7293
provider support
tlongwell-block Oct 9, 2025
42f1591
model override provider
tlongwell-block Oct 9, 2025
882bfcf
fix extension loading
tlongwell-block Oct 9, 2025
ff9bfb8
use recipes directly for run_complete_subagent_task. Support retries.
tlongwell-block Oct 9, 2025
022cf2a
final output support
tlongwell-block Oct 9, 2025
155bb3c
use structs instead of raw json
tlongwell-block Oct 9, 2025
2aa0d34
simplified provider/model overrides
tlongwell-block Oct 9, 2025
76fa1aa
Merge main into minimal-subrecipe-overhaul, keeping our changes for t…
tlongwell-block Oct 10, 2025
eea5c5f
remove no value comments
tlongwell-block Oct 10, 2025
d9e552b
Merge main into minimal-subrecipe-overhaul
tlongwell-block Oct 16, 2025
47fcb80
Fix clippy type complexity warning with type alias
tlongwell-block Oct 16, 2025
1feb393
Fix session creation race condition with mutex
tlongwell-block Oct 16, 2025
a531e4f
parameters in subrcipes and session race condition
tlongwell-block Oct 17, 2025
a731d4f
restore system prompts
tlongwell-block Oct 17, 2025
ba1991c
Merge main into minimal-subrecipe-overhaul-with-main
tlongwell-block Oct 20, 2025
3589e67
Fix merge conflict: Update build_recipe_from_template call signature
tlongwell-block Oct 20, 2025
c344549
comment
tlongwell-block Oct 21, 2025
f07b81e
remove get_task_result, remove bool defaults, RECIPE_TASK_TYPE consta…
tlongwell-block Oct 22, 2025
525766f
pull session_manager.rs from main
tlongwell-block Oct 22, 2025
53b697f
consolidate subrecipe registration and final output tool config
tlongwell-block Oct 22, 2025
b1d1cad
remove `context` from recipes and subagents
tlongwell-block Oct 22, 2025
15e16ae
openapi
tlongwell-block Oct 22, 2025
bccd42d
context removal
tlongwell-block Oct 23, 2025
dde49b5
Merge main into minimal-subrecipe-overhaul-with-main
tlongwell-block Oct 23, 2025
fad7259
feat: Remove ModelOverrideProvider in favor of direct provider instan…
tlongwell-block Nov 5, 2025
f298593
chore: Remove trivial self-explanatory comments from code
tlongwell-block Nov 5, 2025
7b9581f
remove ModelOverrideProvider in favor of get_name
tlongwell-block Nov 5, 2025
82d2c66
trivial comments
tlongwell-block Nov 5, 2025
6eabf57
remove comment
tlongwell-block Nov 6, 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
4 changes: 0 additions & 4 deletions crates/goose-cli/src/recipes/secret_discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,6 @@ mod tests {
available_tools: Vec::new(),
},
]),
context: None,
settings: None,
activities: None,
author: None,
Expand Down Expand Up @@ -210,7 +209,6 @@ mod tests {
instructions: Some("Test instructions".to_string()),
prompt: None,
extensions: None,
context: None,
settings: None,
activities: None,
author: None,
Expand Down Expand Up @@ -255,7 +253,6 @@ mod tests {
available_tools: Vec::new(),
},
]),
context: None,
settings: None,
activities: None,
author: None,
Expand Down Expand Up @@ -309,7 +306,6 @@ mod tests {
sequential_when_repeated: false,
description: None,
}]),
context: None,
settings: None,
activities: None,
author: None,
Expand Down
14 changes: 7 additions & 7 deletions crates/goose-cli/src/session/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,13 +283,13 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
// Create the agent
let agent: Agent = Agent::new();

if let Some(sub_recipes) = session_config.sub_recipes {
agent.add_sub_recipes(sub_recipes).await;
}

if let Some(final_output_response) = session_config.final_output_response {
agent.add_final_output_tool(final_output_response).await;
}
agent
.apply_recipe_components(
session_config.sub_recipes,
session_config.final_output_response,
true,
)
.await;

let new_provider = match create(&provider_name, model_config).await {
Ok(provider) => provider,
Expand Down
16 changes: 7 additions & 9 deletions crates/goose-server/src/routes/recipe_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,15 +156,13 @@ pub async fn apply_recipe_to_agent(
recipe: &Recipe,
include_final_output_tool: bool,
) -> Option<String> {
if let Some(sub_recipes) = &recipe.sub_recipes {
agent.add_sub_recipes(sub_recipes.clone()).await;
}

if include_final_output_tool {
if let Some(response) = &recipe.response {
agent.add_final_output_tool(response.clone()).await;
}
}
agent
.apply_recipe_components(
recipe.sub_recipes.clone(),
recipe.response.clone(),
include_final_output_tool,
)
.await;

recipe.instructions.as_ref().map(|instructions| {
let mut context: HashMap<&str, Value> = HashMap::new();
Expand Down
17 changes: 17 additions & 0 deletions crates/goose/src/agents/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,23 @@ impl Agent {
sub_recipe_manager.add_sub_recipe_tools(sub_recipes);
}

pub async fn apply_recipe_components(
&self,
sub_recipes: Option<Vec<SubRecipe>>,
response: Option<Response>,
include_final_output: bool,
) {
if let Some(sub_recipes) = sub_recipes {
self.add_sub_recipes(sub_recipes).await;
}

if include_final_output {
if let Some(response) = response {
self.add_final_output_tool(response).await;
}
}
}

/// Dispatch a single tool call to the appropriate client
#[instrument(skip(self, tool_call, request_id), fields(input, output))]
pub async fn dispatch_tool_call(
Expand Down
30 changes: 8 additions & 22 deletions crates/goose/src/agents/recipe_tools/dynamic_task_tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::agents::extension::ExtensionConfig;
use crate::agents::subagent_execution_tool::tasks_manager::TasksManager;
use crate::agents::subagent_execution_tool::{
lib::ExecutionMode,
task_types::{Task, TaskType},
task_types::{Task, TaskPayload},
};
use crate::agents::tool_execution::ToolCallResult;
use crate::config::GooseMode;
Expand Down Expand Up @@ -81,9 +81,6 @@ pub struct TaskParameter {
#[serde(skip_serializing_if = "Option::is_none")]
pub retry: Option<JsonObject>,

#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<Vec<String>>,

#[serde(skip_serializing_if = "Option::is_none")]
pub activities: Option<Vec<String>>,

Expand Down Expand Up @@ -116,7 +113,7 @@ pub fn create_dynamic_task_tool() -> Tool {

Tool::new(
DYNAMIC_TASK_TOOL_NAME_PREFIX.to_string(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

above, can you remove the .expects()? it would crash the server if that broke

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This ends up opening a bit of a can of worms. This should get looked at when we move the tools to a platform extension

"Create tasks with instructions or prompt. For simple tasks, only include the instructions field. Extensions control: omit field = use all current extensions; empty array [] = no extensions; array with names = only those extensions. Specify extensions as shortnames (the prefixes for your tools). Specify return_last_only as true and have your subagent summarize its work in its last message to conserve your own context. Optional: title, description, extensions, settings, retry, response schema, context, activities. Arrays for multiple tasks.".to_string(),
"Create tasks with instructions or prompt. For simple tasks, only include the instructions field. Extensions control: omit field = use all current extensions; empty array [] = no extensions; array with names = only those extensions. Specify extensions as shortnames (the prefixes for your tools). Specify return_last_only as true and have your subagent summarize its work in its last message to conserve your own context. Optional: title, description, extensions, settings, retry, response schema, activities. Arrays for multiple tasks.".to_string(),
input_schema,
).annotate(ToolAnnotations {
title: Some("Create Dynamic Tasks".to_string()),
Expand Down Expand Up @@ -228,7 +225,6 @@ pub fn task_params_to_inline_recipe(
builder = apply_if_ok(builder, task_param.get("settings"), RecipeBuilder::settings);
builder = apply_if_ok(builder, task_param.get("response"), RecipeBuilder::response);
builder = apply_if_ok(builder, task_param.get("retry"), RecipeBuilder::retry);
builder = apply_if_ok(builder, task_param.get("context"), RecipeBuilder::context);
builder = apply_if_ok(
builder,
task_param.get("activities"),
Expand Down Expand Up @@ -297,17 +293,6 @@ pub async fn create_dynamic_task(
// All tasks must use the new inline recipe path
match task_params_to_inline_recipe(task_param, &loaded_extensions) {
Ok(recipe) => {
let recipe_json = match serde_json::to_value(&recipe) {
Ok(json) => json,
Err(e) => {
return ToolCallResult::from(Err(ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to serialize recipe: {}", e)),
data: None,
}));
}
};

// Extract return_last_only flag if present
let return_last_only = task_param
.get("return_last_only")
Expand All @@ -316,11 +301,12 @@ pub async fn create_dynamic_task(

let task = Task {
id: uuid::Uuid::new_v4().to_string(),
task_type: TaskType::InlineRecipe,
payload: json!({
"recipe": recipe_json,
"return_last_only": return_last_only
}),
payload: TaskPayload {
recipe,
return_last_only,
sequential_when_repeated: false,
parameter_values: None,
},
};
tasks.push(task);
}
Expand Down
56 changes: 34 additions & 22 deletions crates/goose/src/agents/recipe_tools/sub_recipe_tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ use rmcp::model::{Tool, ToolAnnotations};
use serde_json::{json, Map, Value};

use crate::agents::subagent_execution_tool::lib::ExecutionMode;
use crate::agents::subagent_execution_tool::task_types::{Task, TaskType};
use crate::agents::subagent_execution_tool::task_types::{Task, TaskPayload};
use crate::agents::subagent_execution_tool::tasks_manager::TasksManager;
use crate::recipe::build_recipe::build_recipe_from_template;
use crate::recipe::local_recipes::load_local_recipe_file;
use crate::recipe::{Recipe, RecipeParameter, RecipeParameterRequirement, SubRecipe};

use super::param_utils::prepare_command_params;
Expand Down Expand Up @@ -54,27 +56,37 @@ fn extract_task_parameters(params: &Value) -> Vec<Value> {
fn create_tasks_from_params(
sub_recipe: &SubRecipe,
command_params: &[std::collections::HashMap<String, String>],
) -> Vec<Task> {
let tasks: Vec<Task> = command_params
.iter()
.map(|task_command_param| {
let payload = json!({
"sub_recipe": {
"name": sub_recipe.name.clone(),
"command_parameters": task_command_param,
"recipe_path": sub_recipe.path.clone(),
"sequential_when_repeated": sub_recipe.sequential_when_repeated
}
});
Task {
id: uuid::Uuid::new_v4().to_string(),
task_type: TaskType::SubRecipe,
payload,
}
})
.collect();
) -> Result<Vec<Task>> {
let recipe_file = load_local_recipe_file(&sub_recipe.path)
.map_err(|e| anyhow::anyhow!("Failed to load recipe {}: {}", sub_recipe.path, e))?;

let mut tasks = Vec::new();
for task_command_param in command_params {
let recipe = build_recipe_from_template(
recipe_file.content.clone(),
&recipe_file.parent_dir,
task_command_param
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
None::<fn(&str, &str) -> Result<String, anyhow::Error>>,
)
.map_err(|e| anyhow::anyhow!("Failed to build recipe: {}", e))?;

let task = Task {
id: uuid::Uuid::new_v4().to_string(),
payload: TaskPayload {
recipe,
return_last_only: false,
sequential_when_repeated: sub_recipe.sequential_when_repeated,
parameter_values: Some(task_command_param.clone()),
},
};

tasks.push(task);
}

tasks
Ok(tasks)
}

fn create_task_execution_payload(tasks: &[Task], sub_recipe: &SubRecipe) -> Value {
Expand All @@ -97,7 +109,7 @@ pub async fn create_sub_recipe_task(
) -> Result<String> {
let task_params_array = extract_task_parameters(&params);
let command_params = prepare_command_params(sub_recipe, task_params_array.clone())?;
let tasks = create_tasks_from_params(sub_recipe, &command_params);
let tasks = create_tasks_from_params(sub_recipe, &command_params)?;
let task_execution_payload = create_task_execution_payload(&tasks, sub_recipe);

let tasks_json = serde_json::to_string(&task_execution_payload)
Expand Down
6 changes: 5 additions & 1 deletion crates/goose/src/agents/subagent_execution_tool/lib/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ pub async fn execute_tasks(
}
}
ExecutionMode::Parallel => {
if tasks.iter().any(|task| task.get_sequential_when_repeated()) {
let any_sequential = tasks
.iter()
.any(|task| task.payload.sequential_when_repeated);

if any_sequential {
Ok(json!(
{
"execution_mode": ExecutionMode::Sequential,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ use crate::agents::subagent_execution_tool::notification_events::{
use crate::agents::subagent_execution_tool::task_types::{Task, TaskInfo, TaskResult, TaskStatus};
use crate::agents::subagent_execution_tool::utils::{count_by_status, get_task_name};
use crate::utils::is_token_cancelled;
use serde_json::Value;
use tokio::sync::mpsc::Sender;

const RECIPE_TASK_TYPE: &str = "recipe";

#[derive(Debug, Clone, PartialEq)]
pub enum DisplayMode {
MultipleTasksOutput,
Expand All @@ -28,25 +29,22 @@ const THROTTLE_INTERVAL_MS: u64 = 250;
const COMPLETION_NOTIFICATION_DELAY_MS: u64 = 500;

fn format_task_metadata(task_info: &TaskInfo) -> String {
if let Some(params) = task_info.task.get_command_parameters() {
if params.is_empty() {
return String::new();
// If we have parameter values, format them nicely
if let Some(ref params) = task_info.task.payload.parameter_values {
if !params.is_empty() {
let mut param_strs: Vec<String> = params
.iter()
.filter(|(k, _)| k.as_str() != "recipe_dir")
.map(|(k, v)| format!("{}={}", k, v))
.collect();
if !param_strs.is_empty() {
param_strs.sort();
return param_strs.join(", ");
}
}

params
.iter()
.map(|(key, value)| {
let value_str = match value {
Value::String(s) => s.clone(),
_ => value.to_string(),
};
format!("{}={}", key, value_str)
})
.collect::<Vec<_>>()
.join(",")
} else {
String::new()
}
// Fallback to recipe title if no parameters
task_info.task.payload.recipe.title.clone()
}

pub struct TaskExecutionTracker {
Expand Down Expand Up @@ -151,13 +149,15 @@ impl TaskExecutionTracker {
async fn format_line(&self, task_info: Option<&TaskInfo>, line: &str) -> String {
if let Some(task_info) = task_info {
let task_name = get_task_name(task_info);
let task_type = task_info.task.task_type.clone();
let metadata = format_task_metadata(task_info);

if metadata.is_empty() {
format!("[{} ({})] {}", task_name, task_type, line)
format!("[{} ({})] {}", task_name, RECIPE_TASK_TYPE, line)
} else {
format!("[{} ({}) {}] {}", task_name, task_type, metadata, line)
format!(
"[{} ({}) {}] {}",
task_name, RECIPE_TASK_TYPE, metadata, line
)
}
} else {
line.to_string()
Expand Down Expand Up @@ -232,7 +232,7 @@ impl TaskExecutionTracker {
}
}),
current_output: task_info.current_output.clone(),
task_type: task_info.task.task_type.to_string(),
task_type: RECIPE_TASK_TYPE.to_string(),
task_name: get_task_name(task_info).to_string(),
task_metadata: format_task_metadata(task_info),
error: task_info.error().cloned(),
Expand Down
Loading