Fix: Subagent tool can execute existing tasks by ID

The `subagent` tool now accepts an optional `task_id` parameter.
If a `task_id` is provided, the tool will execute the existing task
from the `TasksManager` instead of creating a new one. This resolves
a regression where tasks created by file-based sub-recipes were
orphaned and could not be executed.

- Added `task_id: Option<String>` to `SubagentParams`.
- Updated `create_subagent_tool` documentation.
- Modified `impl TryFrom<SubagentParams> for Recipe` to allow partial
  recipe creation if `task_id` is present, providing a dummy instruction
  if needed to satisfy the builder.
- Updated `run_subagent_tool` to prioritize execution of existing tasks
  by `task_id` if provided, otherwise proceeds with new task creation.
- Added `subagent_tool_test.rs` to verify new functionality.
This commit is contained in:
Tyler Longwell
2025-11-24 13:24:02 -05:00
parent b046fbb5b7
commit 88b98eaa8d
3 changed files with 83 additions and 3 deletions
@@ -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;
@@ -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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[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<SubagentParams> for Recipe {
type Error = anyhow::Error;
fn try_from(params: SubagentParams) -> Result<Self, Self::Error> {
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<SubagentParams> 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);
}
@@ -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());
}