use agent manager for subagent (#4828)
This commit is contained in:
@@ -23,6 +23,7 @@ use crate::agents::recipe_tools::dynamic_task_tools::{
|
||||
use crate::agents::retry::{RetryManager, RetryResult};
|
||||
use crate::agents::router_tools::ROUTER_LLM_SEARCH_TOOL_NAME;
|
||||
use crate::agents::sub_recipe_manager::SubRecipeManager;
|
||||
use crate::agents::subagent_execution_tool::lib::ExecutionMode;
|
||||
use crate::agents::subagent_execution_tool::subagent_execute_task_tool::{
|
||||
self, SUBAGENT_EXECUTE_TASK_TOOL_NAME,
|
||||
};
|
||||
@@ -297,6 +298,7 @@ impl Agent {
|
||||
permission_check_result: &PermissionCheckResult,
|
||||
message_tool_response: Arc<Mutex<Message>>,
|
||||
cancel_token: Option<tokio_util::sync::CancellationToken>,
|
||||
session: Option<SessionConfig>,
|
||||
) -> Result<Vec<(String, ToolStream)>> {
|
||||
let mut tool_futures: Vec<(String, ToolStream)> = Vec::new();
|
||||
|
||||
@@ -304,7 +306,12 @@ impl Agent {
|
||||
for request in &permission_check_result.approved {
|
||||
if let Ok(tool_call) = request.tool_call.clone() {
|
||||
let (req_id, tool_result) = self
|
||||
.dispatch_tool_call(tool_call, request.id.clone(), cancel_token.clone())
|
||||
.dispatch_tool_call(
|
||||
tool_call,
|
||||
request.id.clone(),
|
||||
cancel_token.clone(),
|
||||
session.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
tool_futures.push((
|
||||
@@ -384,6 +391,7 @@ impl Agent {
|
||||
tool_call: CallToolRequestParam,
|
||||
request_id: String,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
session: Option<SessionConfig>,
|
||||
) -> (String, Result<ToolCallResult, ErrorData>) {
|
||||
if tool_call.name == PLATFORM_MANAGE_SCHEDULE_TOOL_NAME {
|
||||
let arguments = tool_call
|
||||
@@ -451,16 +459,89 @@ impl Agent {
|
||||
.dispatch_sub_recipe_tool_call(&tool_call.name, arguments, &self.tasks_manager)
|
||||
.await
|
||||
} else if tool_call.name == SUBAGENT_EXECUTE_TASK_TOOL_NAME {
|
||||
let provider = self.provider().await.ok();
|
||||
let arguments = tool_call
|
||||
.arguments
|
||||
.clone()
|
||||
.map(Value::Object)
|
||||
.unwrap_or(Value::Object(serde_json::Map::new()));
|
||||
let provider = match self.provider().await {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
return (
|
||||
request_id,
|
||||
Err(ErrorData::new(
|
||||
ErrorCode::INTERNAL_ERROR,
|
||||
"Provider is required".to_string(),
|
||||
None,
|
||||
)),
|
||||
);
|
||||
}
|
||||
};
|
||||
let session = match session.as_ref() {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
return (
|
||||
request_id,
|
||||
Err(ErrorData::new(
|
||||
ErrorCode::INTERNAL_ERROR,
|
||||
"Session is required".to_string(),
|
||||
None,
|
||||
)),
|
||||
);
|
||||
}
|
||||
};
|
||||
let parent_session_id = session.id.to_string();
|
||||
let parent_working_dir = session.working_dir.clone();
|
||||
|
||||
let task_config = TaskConfig::new(
|
||||
provider,
|
||||
parent_session_id,
|
||||
parent_working_dir,
|
||||
get_enabled_extensions(),
|
||||
);
|
||||
|
||||
let arguments = match tool_call.arguments.clone() {
|
||||
Some(args) => Value::Object(args),
|
||||
None => {
|
||||
return (
|
||||
request_id,
|
||||
Err(ErrorData::new(
|
||||
ErrorCode::INVALID_PARAMS,
|
||||
"Tool call arguments are required".to_string(),
|
||||
None,
|
||||
)),
|
||||
);
|
||||
}
|
||||
};
|
||||
let task_ids: Vec<String> = match arguments.get("task_ids") {
|
||||
Some(v) => match serde_json::from_value(v.clone()) {
|
||||
Ok(ids) => ids,
|
||||
Err(_) => {
|
||||
return (
|
||||
request_id,
|
||||
Err(ErrorData::new(
|
||||
ErrorCode::INVALID_PARAMS,
|
||||
"Invalid task_ids format".to_string(),
|
||||
None,
|
||||
)),
|
||||
);
|
||||
}
|
||||
},
|
||||
None => {
|
||||
return (
|
||||
request_id,
|
||||
Err(ErrorData::new(
|
||||
ErrorCode::INVALID_PARAMS,
|
||||
"task_ids parameter is required".to_string(),
|
||||
None,
|
||||
)),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let execution_mode = arguments
|
||||
.get("execution_mode")
|
||||
.and_then(|v| serde_json::from_value::<ExecutionMode>(v.clone()).ok())
|
||||
.unwrap_or(ExecutionMode::Sequential);
|
||||
|
||||
let task_config = TaskConfig::new(provider);
|
||||
subagent_execute_task_tool::run_tasks(
|
||||
arguments,
|
||||
task_ids,
|
||||
execution_mode,
|
||||
task_config,
|
||||
&self.tasks_manager,
|
||||
cancellation_token,
|
||||
@@ -1162,6 +1243,7 @@ impl Agent {
|
||||
&permission_check_result,
|
||||
message_tool_response.clone(),
|
||||
cancel_token.clone(),
|
||||
session.clone(),
|
||||
).await?;
|
||||
|
||||
let tool_futures_arc = Arc::new(Mutex::new(tool_futures));
|
||||
@@ -1172,6 +1254,7 @@ impl Agent {
|
||||
tool_futures_arc.clone(),
|
||||
message_tool_response.clone(),
|
||||
cancel_token.clone(),
|
||||
session.clone(),
|
||||
&inspection_results,
|
||||
);
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ mod router_tool_selector;
|
||||
mod router_tools;
|
||||
mod schedule_tool;
|
||||
pub mod sub_recipe_manager;
|
||||
pub mod subagent;
|
||||
pub mod subagent_execution_tool;
|
||||
pub mod subagent_handler;
|
||||
mod subagent_task_config;
|
||||
@@ -30,6 +29,5 @@ pub use agent::{Agent, AgentEvent};
|
||||
pub use extension::ExtensionConfig;
|
||||
pub use extension_manager::ExtensionManager;
|
||||
pub use prompt_manager::PromptManager;
|
||||
pub use subagent::{SubAgent, SubAgentProgress, SubAgentStatus};
|
||||
pub use subagent_task_config::TaskConfig;
|
||||
pub use types::{FrontendTool, RetryConfig, SessionConfig, SuccessCheck};
|
||||
|
||||
@@ -84,48 +84,6 @@ impl Agent {
|
||||
Ok((tools, toolshim_tools, system_prompt))
|
||||
}
|
||||
|
||||
/// Generate a response from the LLM provider
|
||||
/// Handles toolshim transformations if needed
|
||||
pub(crate) async fn generate_response_from_provider(
|
||||
provider: Arc<dyn Provider>,
|
||||
system_prompt: &str,
|
||||
messages: &[Message],
|
||||
tools: &[Tool],
|
||||
toolshim_tools: &[Tool],
|
||||
) -> Result<(Message, ProviderUsage), ProviderError> {
|
||||
let config = provider.get_model_config();
|
||||
|
||||
// Convert tool messages to text if toolshim is enabled
|
||||
let messages_for_provider = if config.toolshim {
|
||||
convert_tool_messages_to_text(messages)
|
||||
} else {
|
||||
Conversation::new_unvalidated(messages.to_vec())
|
||||
};
|
||||
|
||||
// Call the provider to get a response
|
||||
let (mut response, mut usage) = provider
|
||||
.complete(system_prompt, messages_for_provider.messages(), tools)
|
||||
.await?;
|
||||
|
||||
// Ensure we have token counts, estimating if necessary
|
||||
usage
|
||||
.ensure_tokens(
|
||||
system_prompt,
|
||||
messages_for_provider.messages(),
|
||||
&response,
|
||||
tools,
|
||||
)
|
||||
.await?;
|
||||
|
||||
crate::providers::base::set_current_model(&usage.model);
|
||||
|
||||
if config.toolshim {
|
||||
response = toolshim_postprocess(response, toolshim_tools).await?;
|
||||
}
|
||||
|
||||
Ok((response, usage))
|
||||
}
|
||||
|
||||
/// Stream a response from the LLM provider.
|
||||
/// Handles toolshim transformations if needed
|
||||
pub(crate) async fn stream_response_from_provider(
|
||||
|
||||
@@ -1,334 +0,0 @@
|
||||
use crate::agents::subagent_task_config::DEFAULT_SUBAGENT_MAX_TURNS;
|
||||
use crate::{
|
||||
agents::{extension_manager::ExtensionManager, Agent, TaskConfig},
|
||||
config::get_all_extensions,
|
||||
prompt_template::render_global_file,
|
||||
providers::errors::ProviderError,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use chrono::{DateTime, Utc};
|
||||
use rmcp::model::Tool;
|
||||
use rmcp::model::{ErrorCode, ErrorData};
|
||||
use serde::{Deserialize, Serialize};
|
||||
// use serde_json::{self};
|
||||
use crate::conversation::message::{Message, MessageContent, ToolRequest};
|
||||
use crate::conversation::Conversation;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{debug, error, instrument};
|
||||
|
||||
/// Status of a subagent
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum SubAgentStatus {
|
||||
Ready, // Ready to process messages
|
||||
Processing, // Currently working on a task
|
||||
Completed(String), // Task completed (with optional message for success/error)
|
||||
Terminated, // Manually terminated
|
||||
}
|
||||
|
||||
/// Progress information for a subagent
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SubAgentProgress {
|
||||
pub subagent_id: String,
|
||||
pub status: SubAgentStatus,
|
||||
pub message: String,
|
||||
pub turn: usize,
|
||||
pub max_turns: Option<usize>,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// A specialized agent that can handle specific tasks independently
|
||||
pub struct SubAgent {
|
||||
pub id: String,
|
||||
pub conversation: Arc<Mutex<Conversation>>,
|
||||
pub status: Arc<RwLock<SubAgentStatus>>,
|
||||
pub config: TaskConfig,
|
||||
pub turn_count: Arc<Mutex<usize>>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub extension_manager: Arc<RwLock<ExtensionManager>>,
|
||||
}
|
||||
|
||||
impl SubAgent {
|
||||
/// Create a new subagent with the given configuration and provider
|
||||
#[instrument(skip(task_config))]
|
||||
pub async fn new(task_config: TaskConfig) -> Result<Arc<Self>, anyhow::Error> {
|
||||
debug!("Creating new subagent with id: {}", task_config.id);
|
||||
|
||||
// Create a new extension manager for this subagent
|
||||
let extension_manager = ExtensionManager::new();
|
||||
|
||||
// Determine which extensions to add:
|
||||
// 1. If task_config.extensions is Some(vec), use those specific extensions
|
||||
// 2. If task_config.extensions is None, use all enabled extensions (backward compatibility)
|
||||
|
||||
let extensions_to_add = if let Some(ref extensions) = task_config.extensions {
|
||||
// Use the explicitly specified extensions
|
||||
extensions.clone()
|
||||
} else {
|
||||
// Default behavior: use all enabled extensions
|
||||
get_all_extensions()
|
||||
.into_iter()
|
||||
.filter(|ext| ext.enabled)
|
||||
.map(|ext| ext.config)
|
||||
.collect()
|
||||
};
|
||||
|
||||
// Add the determined extensions to the subagent's extension manager
|
||||
for extension in extensions_to_add {
|
||||
if let Err(e) = extension_manager.add_extension(extension).await {
|
||||
debug!("Failed to add extension to subagent: {}", e);
|
||||
// Continue with other extensions even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
let subagent = Arc::new(SubAgent {
|
||||
id: task_config.id.clone(),
|
||||
conversation: Arc::new(Mutex::new(Conversation::new_unvalidated(Vec::new()))),
|
||||
status: Arc::new(RwLock::new(SubAgentStatus::Ready)),
|
||||
config: task_config,
|
||||
turn_count: Arc::new(Mutex::new(0)),
|
||||
created_at: Utc::now(),
|
||||
extension_manager: Arc::new(RwLock::new(extension_manager)),
|
||||
});
|
||||
|
||||
debug!("Subagent {} created successfully", subagent.id);
|
||||
Ok(subagent)
|
||||
}
|
||||
|
||||
/// Update the status of the subagent
|
||||
async fn set_status(&self, status: SubAgentStatus) {
|
||||
// Update the status first, then release the lock
|
||||
{
|
||||
let mut current_status = self.status.write().await;
|
||||
*current_status = status.clone();
|
||||
} // Write lock is released here!
|
||||
}
|
||||
|
||||
/// Process a message and generate a response using the subagent's provider
|
||||
#[instrument(skip(self, message))]
|
||||
pub async fn reply_subagent(
|
||||
&self,
|
||||
message: String,
|
||||
task_config: TaskConfig,
|
||||
) -> Result<Conversation, anyhow::Error> {
|
||||
debug!("Processing message for subagent {}", self.id);
|
||||
|
||||
// Get provider from task config
|
||||
let provider = self
|
||||
.config
|
||||
.provider
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("No provider configured for subagent"))?;
|
||||
|
||||
// Set status to processing
|
||||
self.set_status(SubAgentStatus::Processing).await;
|
||||
|
||||
// Add user message to conversation
|
||||
let user_message = Message::user().with_text(message.clone());
|
||||
{
|
||||
let mut conversation = self.conversation.lock().await;
|
||||
conversation.push(user_message.clone());
|
||||
}
|
||||
|
||||
// Get the current conversation for context
|
||||
let mut messages = {
|
||||
let conversation = self.conversation.lock().await;
|
||||
conversation.clone()
|
||||
};
|
||||
|
||||
// Get tools from the subagent's own extension manager
|
||||
let tools: Vec<Tool> = self
|
||||
.extension_manager
|
||||
.read()
|
||||
.await
|
||||
.get_prefixed_tools(None)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let toolshim_tools: Vec<Tool> = vec![];
|
||||
|
||||
// Build system prompt using the template
|
||||
let system_prompt = self.build_system_prompt(&tools).await?;
|
||||
|
||||
// Generate response from provider with loop for tool processing (max_turns iterations)
|
||||
let mut loop_count = 0;
|
||||
let max_turns = self.config.max_turns.unwrap_or(DEFAULT_SUBAGENT_MAX_TURNS);
|
||||
let mut last_error: Option<anyhow::Error> = None;
|
||||
|
||||
// Generate response from provider
|
||||
loop {
|
||||
loop_count += 1;
|
||||
|
||||
match Agent::generate_response_from_provider(
|
||||
Arc::clone(provider),
|
||||
&system_prompt,
|
||||
messages.messages(),
|
||||
&tools,
|
||||
&toolshim_tools,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((response, _usage)) => {
|
||||
// Process any tool calls in the response
|
||||
let tool_requests: Vec<ToolRequest> = response
|
||||
.content
|
||||
.iter()
|
||||
.filter_map(|content| {
|
||||
if let MessageContent::ToolRequest(req) = content {
|
||||
Some(req.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// If there are no tool requests, we're done
|
||||
if tool_requests.is_empty() || loop_count >= max_turns {
|
||||
self.add_message(response.clone()).await;
|
||||
messages.push(response.clone());
|
||||
|
||||
// Set status back to ready
|
||||
self.set_status(SubAgentStatus::Completed("Completed!".to_string()))
|
||||
.await;
|
||||
break;
|
||||
}
|
||||
|
||||
// Add the assistant message with tool calls to the conversation
|
||||
messages.push(response.clone());
|
||||
|
||||
// Process each tool request and create user response messages
|
||||
for request in &tool_requests {
|
||||
if let Ok(tool_call) = &request.tool_call {
|
||||
// Handle platform tools or dispatch to extension manager
|
||||
let tool_result = match self
|
||||
.extension_manager
|
||||
.read()
|
||||
.await
|
||||
.dispatch_tool_call(tool_call.clone(), CancellationToken::default())
|
||||
.await
|
||||
{
|
||||
Ok(result) => result.result.await,
|
||||
Err(e) => Err(ErrorData::new(
|
||||
ErrorCode::INTERNAL_ERROR,
|
||||
e.to_string(),
|
||||
None,
|
||||
)),
|
||||
};
|
||||
|
||||
match tool_result {
|
||||
Ok(result) => {
|
||||
// Create a user message with the tool response
|
||||
let tool_response_message = Message::user()
|
||||
.with_tool_response(request.id.clone(), Ok(result.clone()));
|
||||
messages.push(tool_response_message);
|
||||
}
|
||||
Err(e) => {
|
||||
// Create a user message with the tool error
|
||||
let tool_error_message = Message::user().with_tool_response(
|
||||
request.id.clone(),
|
||||
Err(ErrorData::new(
|
||||
ErrorCode::INTERNAL_ERROR,
|
||||
e.to_string(),
|
||||
None,
|
||||
)),
|
||||
);
|
||||
messages.push(tool_error_message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Continue the loop to get the next response from the provider
|
||||
}
|
||||
Err(ProviderError::ContextLengthExceeded(_)) => {
|
||||
self.set_status(SubAgentStatus::Completed(
|
||||
"Context length exceeded".to_string(),
|
||||
))
|
||||
.await;
|
||||
last_error = Some(anyhow::anyhow!("Context length exceeded"));
|
||||
break;
|
||||
}
|
||||
Err(ProviderError::RateLimitExceeded { .. }) => {
|
||||
self.set_status(SubAgentStatus::Completed("Rate limit exceeded".to_string()))
|
||||
.await;
|
||||
last_error = Some(anyhow::anyhow!("Rate limit exceeded"));
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
self.set_status(SubAgentStatus::Completed(format!("Error: {}", e)))
|
||||
.await;
|
||||
error!("Error: {}", e);
|
||||
last_error = Some(anyhow::anyhow!("Provider error: {}", e));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle error cases or return the last message
|
||||
if let Some(error) = last_error {
|
||||
Err(error)
|
||||
} else {
|
||||
Ok(messages)
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a message to the conversation (for tracking agent responses)
|
||||
async fn add_message(&self, message: Message) {
|
||||
let mut conversation = self.conversation.lock().await;
|
||||
conversation.push(message);
|
||||
}
|
||||
|
||||
/// Build the system prompt for the subagent using the template
|
||||
async fn build_system_prompt(&self, available_tools: &[Tool]) -> Result<String, anyhow::Error> {
|
||||
let mut context = HashMap::new();
|
||||
|
||||
// Add basic context
|
||||
context.insert(
|
||||
"current_date_time",
|
||||
serde_json::Value::String(Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string()),
|
||||
);
|
||||
context.insert("subagent_id", serde_json::Value::String(self.id.clone()));
|
||||
|
||||
// Add max turns if configured
|
||||
if let Some(max_turns) = self.config.max_turns {
|
||||
context.insert(
|
||||
"max_turns",
|
||||
serde_json::Value::Number(serde_json::Number::from(max_turns)),
|
||||
);
|
||||
}
|
||||
|
||||
// Add available tools with descriptions for better context
|
||||
let tools_with_descriptions: Vec<String> = available_tools
|
||||
.iter()
|
||||
.map(|t| {
|
||||
if let Some(description) = &t.description {
|
||||
format!("{}: {}", t.name, description)
|
||||
} else {
|
||||
t.name.to_string()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
context.insert(
|
||||
"available_tools",
|
||||
serde_json::Value::String(if tools_with_descriptions.is_empty() {
|
||||
"None".to_string()
|
||||
} else {
|
||||
tools_with_descriptions.join(", ")
|
||||
}),
|
||||
);
|
||||
|
||||
// Add tool count for context
|
||||
context.insert(
|
||||
"tool_count",
|
||||
serde_json::Value::Number(serde_json::Number::from(available_tools.len())),
|
||||
);
|
||||
|
||||
// Render the subagent system prompt template
|
||||
let system_prompt = render_global_file("subagent_system.md", &context)
|
||||
.map_err(|e| anyhow!("Failed to render subagent system prompt: {}", e))?;
|
||||
|
||||
Ok(system_prompt)
|
||||
}
|
||||
}
|
||||
@@ -12,21 +12,13 @@ use tokio::sync::mpsc::Sender;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub async fn execute_tasks(
|
||||
input: Value,
|
||||
task_ids: Vec<String>,
|
||||
execution_mode: ExecutionMode,
|
||||
notifier: Sender<ServerNotification>,
|
||||
task_config: TaskConfig,
|
||||
tasks_manager: &TasksManager,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> Result<Value, String> {
|
||||
let task_ids: Vec<String> = serde_json::from_value(
|
||||
input
|
||||
.get("task_ids")
|
||||
.ok_or("Missing task_ids field")?
|
||||
.clone(),
|
||||
)
|
||||
.map_err(|e| format!("Failed to parse task_ids: {}", e))?;
|
||||
|
||||
let tasks = tasks_manager.get_tasks(&task_ids).await?;
|
||||
|
||||
let task_count = tasks.len();
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use rmcp::model::{Content, ErrorCode, ErrorData, ServerNotification, Tool, ToolAnnotations};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::agents::subagent_task_config::TaskConfig;
|
||||
use crate::agents::{
|
||||
subagent_execution_tool::lib::execute_tasks,
|
||||
subagent_execution_tool::task_types::ExecutionMode,
|
||||
subagent_execution_tool::tasks_manager::TasksManager, tool_execution::ToolCallResult,
|
||||
};
|
||||
use rmcp::model::{Content, ErrorCode, ErrorData, ServerNotification, Tool, ToolAnnotations};
|
||||
use rmcp::object;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_stream;
|
||||
@@ -62,7 +60,8 @@ pub fn create_subagent_execute_task_tool() -> Tool {
|
||||
}
|
||||
|
||||
pub async fn run_tasks(
|
||||
execute_data: Value,
|
||||
task_ids: Vec<String>,
|
||||
execution_mode: ExecutionMode,
|
||||
task_config: TaskConfig,
|
||||
tasks_manager: &TasksManager,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
@@ -71,14 +70,8 @@ pub async fn run_tasks(
|
||||
|
||||
let tasks_manager_clone = tasks_manager.clone();
|
||||
let result_future = async move {
|
||||
let execute_data_clone = execute_data.clone();
|
||||
let execution_mode = execute_data_clone
|
||||
.get("execution_mode")
|
||||
.and_then(|v| serde_json::from_value::<ExecutionMode>(v.clone()).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
match execute_tasks(
|
||||
execute_data,
|
||||
task_ids,
|
||||
execution_mode,
|
||||
notification_tx,
|
||||
task_config,
|
||||
|
||||
@@ -74,7 +74,7 @@ async fn handle_inline_recipe_task(
|
||||
mut task_config: TaskConfig,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Result<Value, String> {
|
||||
use crate::agents::subagent_handler::run_complete_subagent_task_with_options;
|
||||
use crate::agents::subagent_handler::run_complete_subagent_task;
|
||||
use crate::recipe::Recipe;
|
||||
|
||||
let recipe_value = task
|
||||
@@ -91,14 +91,23 @@ async fn handle_inline_recipe_task(
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
task_config.extensions = recipe.extensions.clone();
|
||||
if let Some(exts) = recipe.extensions {
|
||||
if !exts.is_empty() {
|
||||
task_config.extensions = exts.clone();
|
||||
}
|
||||
}
|
||||
|
||||
let instruction = recipe
|
||||
.instructions
|
||||
.or(recipe.prompt)
|
||||
.ok_or_else(|| "No instructions or prompt in recipe".to_string())?;
|
||||
|
||||
let result = tokio::select! {
|
||||
result = run_complete_subagent_task_with_options(instruction, task_config, return_last_only) => result,
|
||||
result = run_complete_subagent_task(
|
||||
instruction,
|
||||
task_config,
|
||||
return_last_only,
|
||||
) => result,
|
||||
_ = cancellation_token.cancelled() => {
|
||||
return Err("Task cancelled".to_string());
|
||||
}
|
||||
|
||||
@@ -1,35 +1,30 @@
|
||||
use crate::agents::subagent::SubAgent;
|
||||
use crate::agents::subagent_task_config::TaskConfig;
|
||||
use anyhow::Result;
|
||||
use crate::{
|
||||
agents::{subagent_task_config::TaskConfig, AgentEvent, SessionConfig},
|
||||
conversation::{message::Message, Conversation},
|
||||
execution::manager::AgentManager,
|
||||
session::SessionManager,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::future::BoxFuture;
|
||||
use futures::StreamExt;
|
||||
use rmcp::model::{ErrorCode, ErrorData};
|
||||
|
||||
/// Standalone function to run a complete subagent task
|
||||
pub async fn run_complete_subagent_task(
|
||||
text_instruction: String,
|
||||
task_config: TaskConfig,
|
||||
) -> Result<String, anyhow::Error> {
|
||||
run_complete_subagent_task_with_options(text_instruction, task_config, false).await
|
||||
}
|
||||
use tracing::debug;
|
||||
|
||||
/// Standalone function to run a complete subagent task with output options
|
||||
pub async fn run_complete_subagent_task_with_options(
|
||||
pub async fn run_complete_subagent_task(
|
||||
text_instruction: String,
|
||||
task_config: TaskConfig,
|
||||
return_last_only: bool,
|
||||
) -> Result<String, anyhow::Error> {
|
||||
// Create the subagent with the parent agent's provider
|
||||
let subagent = SubAgent::new(task_config.clone()).await.map_err(|e| {
|
||||
ErrorData::new(
|
||||
ErrorCode::INTERNAL_ERROR,
|
||||
format!("Failed to create subagent: {}", e),
|
||||
None,
|
||||
)
|
||||
})?;
|
||||
|
||||
// Execute the subagent task
|
||||
let messages = subagent
|
||||
.reply_subagent(text_instruction, task_config)
|
||||
.await?;
|
||||
let messages = get_agent_messages(text_instruction, task_config)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
ErrorData::new(
|
||||
ErrorCode::INTERNAL_ERROR,
|
||||
format!("Failed to execute task: {}", e),
|
||||
None,
|
||||
)
|
||||
})?;
|
||||
|
||||
// Extract text content based on return_last_only flag
|
||||
let response_text = if return_last_only {
|
||||
@@ -94,3 +89,73 @@ pub async fn run_complete_subagent_task_with_options(
|
||||
// Return the result
|
||||
Ok(response_text)
|
||||
}
|
||||
|
||||
fn get_agent_messages(
|
||||
text_instruction: String,
|
||||
task_config: TaskConfig,
|
||||
) -> BoxFuture<'static, Result<Conversation>> {
|
||||
Box::pin(async move {
|
||||
let agent_manager = AgentManager::instance()
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to create AgentManager: {}", e))?;
|
||||
let parent_session_id = task_config.parent_session_id;
|
||||
let working_dir = task_config.parent_working_dir;
|
||||
let session = SessionManager::create_session(
|
||||
working_dir.clone(),
|
||||
format!("Subagent task for: {}", parent_session_id),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to create a session for sub agent: {}", e))?;
|
||||
|
||||
let agent = agent_manager
|
||||
.get_or_create_agent(session.id.clone())
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to get sub agent session file path: {}", e))?;
|
||||
agent
|
||||
.update_provider(task_config.provider)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to set provider on sub agent: {}", e))?;
|
||||
|
||||
for extension in task_config.extensions {
|
||||
if let Err(e) = agent.add_extension(extension.clone()).await {
|
||||
debug!(
|
||||
"Failed to add extension '{}' to subagent: {}",
|
||||
extension.name(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut session_messages =
|
||||
Conversation::new_unvalidated(
|
||||
vec![Message::user().with_text(text_instruction.clone())],
|
||||
);
|
||||
let session_config = SessionConfig {
|
||||
id: session.id,
|
||||
working_dir,
|
||||
schedule_id: None,
|
||||
execution_mode: None,
|
||||
max_turns: task_config.max_turns.map(|v| v as u32),
|
||||
retry_config: None,
|
||||
};
|
||||
|
||||
let mut stream = agent
|
||||
.reply(session_messages.clone(), Some(session_config), None)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to get reply from agent: {}", e))?;
|
||||
while let Some(message_result) = stream.next().await {
|
||||
match message_result {
|
||||
Ok(AgentEvent::Message(msg)) => session_messages.push(msg),
|
||||
Ok(AgentEvent::McpNotification(_))
|
||||
| Ok(AgentEvent::ModelChange { .. })
|
||||
| Ok(AgentEvent::HistoryReplaced(_)) => {} // Handle informational events
|
||||
Err(e) => {
|
||||
tracing::error!("Error receiving message from subagent: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(session_messages)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::agents::ExtensionConfig;
|
||||
use crate::providers::base::Provider;
|
||||
use std::env;
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Default maximum number of turns for task execution
|
||||
pub const DEFAULT_SUBAGENT_MAX_TURNS: usize = 25;
|
||||
@@ -13,17 +14,19 @@ pub const GOOSE_SUBAGENT_MAX_TURNS_ENV_VAR: &str = "GOOSE_SUBAGENT_MAX_TURNS";
|
||||
/// Configuration for task execution with all necessary dependencies
|
||||
#[derive(Clone)]
|
||||
pub struct TaskConfig {
|
||||
pub id: String,
|
||||
pub provider: Option<Arc<dyn Provider>>,
|
||||
pub provider: Arc<dyn Provider>,
|
||||
pub parent_session_id: String,
|
||||
pub parent_working_dir: PathBuf,
|
||||
pub extensions: Vec<ExtensionConfig>,
|
||||
pub max_turns: Option<usize>,
|
||||
pub extensions: Option<Vec<crate::agents::extension::ExtensionConfig>>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for TaskConfig {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("TaskConfig")
|
||||
.field("id", &self.id)
|
||||
.field("provider", &"<dyn Provider>")
|
||||
.field("parent_session_id", &self.parent_session_id)
|
||||
.field("parent_working_dir", &self.parent_working_dir)
|
||||
.field("max_turns", &self.max_turns)
|
||||
.field("extensions", &self.extensions)
|
||||
.finish()
|
||||
@@ -32,22 +35,23 @@ impl fmt::Debug for TaskConfig {
|
||||
|
||||
impl TaskConfig {
|
||||
/// Create a new TaskConfig with all required dependencies
|
||||
pub fn new(provider: Option<Arc<dyn Provider>>) -> Self {
|
||||
pub fn new(
|
||||
provider: Arc<dyn Provider>,
|
||||
parent_session_id: String,
|
||||
parent_working_dir: PathBuf,
|
||||
extensions: Vec<ExtensionConfig>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
provider,
|
||||
parent_session_id,
|
||||
parent_working_dir,
|
||||
extensions,
|
||||
max_turns: Some(
|
||||
env::var(GOOSE_SUBAGENT_MAX_TURNS_ENV_VAR)
|
||||
.ok()
|
||||
.and_then(|val| val.parse::<usize>().ok())
|
||||
.unwrap_or(DEFAULT_SUBAGENT_MAX_TURNS),
|
||||
),
|
||||
extensions: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the provider
|
||||
pub fn provider(&self) -> Option<&Arc<dyn Provider>> {
|
||||
self.provider.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ impl From<ToolResult<Vec<Content>>> for ToolCallResult {
|
||||
}
|
||||
|
||||
use super::agent::{tool_stream, ToolStream};
|
||||
use crate::agents::Agent;
|
||||
use crate::agents::{Agent, SessionConfig};
|
||||
use crate::conversation::message::{Message, ToolRequest};
|
||||
use crate::tool_inspection::get_security_finding_id_from_results;
|
||||
|
||||
@@ -53,6 +53,7 @@ impl Agent {
|
||||
tool_futures: Arc<Mutex<Vec<(String, ToolStream)>>>,
|
||||
message_tool_response: Arc<Mutex<Message>>,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
session: Option<SessionConfig>,
|
||||
inspection_results: &'a [crate::tool_inspection::InspectionResult],
|
||||
) -> BoxStream<'a, anyhow::Result<Message>> {
|
||||
try_stream! {
|
||||
@@ -90,7 +91,7 @@ impl Agent {
|
||||
}
|
||||
|
||||
if confirmation.permission == Permission::AllowOnce || confirmation.permission == Permission::AlwaysAllow {
|
||||
let (req_id, tool_result) = self.dispatch_tool_call(tool_call.clone(), request.id.clone(), cancellation_token.clone()).await;
|
||||
let (req_id, tool_result) = self.dispatch_tool_call(tool_call.clone(), request.id.clone(), cancellation_token.clone(), session.clone()).await;
|
||||
let mut futures = tool_futures.lock().await;
|
||||
|
||||
futures.push((req_id, match tool_result {
|
||||
|
||||
@@ -630,7 +630,7 @@ mod final_output_tool_tests {
|
||||
};
|
||||
|
||||
let (_, result) = agent
|
||||
.dispatch_tool_call(tool_call, "request_id".to_string(), None)
|
||||
.dispatch_tool_call(tool_call, "request_id".to_string(), None, None)
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok(), "Tool call should succeed");
|
||||
|
||||
@@ -817,7 +817,7 @@ async fn test_schedule_tool_dispatch() {
|
||||
};
|
||||
|
||||
let (request_id, result) = agent
|
||||
.dispatch_tool_call(tool_call, "test_dispatch".to_string(), None)
|
||||
.dispatch_tool_call(tool_call, "test_dispatch".to_string(), None, None)
|
||||
.await;
|
||||
assert_eq!(request_id, "test_dispatch");
|
||||
assert!(result.is_ok());
|
||||
|
||||
Reference in New Issue
Block a user