reuse the cancellation token in the agent level (#3599)
This commit is contained in:
@@ -273,6 +273,7 @@ impl Agent {
|
||||
&self,
|
||||
tool_call: mcp_core::tool::ToolCall,
|
||||
request_id: String,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> (String, Result<ToolCallResult, ToolError>) {
|
||||
// Check if this tool call should be allowed based on repetition monitoring
|
||||
if let Some(monitor) = self.tool_monitor.lock().await.as_mut() {
|
||||
@@ -345,10 +346,12 @@ impl Agent {
|
||||
|
||||
let task_config =
|
||||
TaskConfig::new(provider, Some(Arc::clone(&self.extension_manager)), mcp_tx);
|
||||
|
||||
subagent_execute_task_tool::run_tasks(
|
||||
tool_call.arguments.clone(),
|
||||
task_config,
|
||||
&self.tasks_manager,
|
||||
cancellation_token,
|
||||
)
|
||||
.await
|
||||
} else if tool_call.name == DYNAMIC_TASK_TOOL_NAME_PREFIX {
|
||||
@@ -914,7 +917,7 @@ 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())
|
||||
.dispatch_tool_call(tool_call, request.id.clone(), cancel_token.clone())
|
||||
.await;
|
||||
|
||||
tool_futures.push((
|
||||
@@ -951,6 +954,7 @@ impl Agent {
|
||||
tool_futures_arc.clone(),
|
||||
&mut permission_manager,
|
||||
message_tool_response.clone(),
|
||||
cancel_token.clone(),
|
||||
);
|
||||
|
||||
while let Some(msg) = tool_approval_stream.try_next().await? {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
mod executor;
|
||||
pub mod lib;
|
||||
pub mod notification_events;
|
||||
pub mod sub_recipe_execute_task_tool;
|
||||
mod task_execution_tracker;
|
||||
mod task_types;
|
||||
mod tasks;
|
||||
pub mod tasks_manager;
|
||||
pub mod utils;
|
||||
mod workers;
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
use serde_json::Value;
|
||||
use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::process::Command;
|
||||
|
||||
use crate::agents::sub_recipe_execution_tool::task_execution_tracker::TaskExecutionTracker;
|
||||
use crate::agents::sub_recipe_execution_tool::task_types::{Task, TaskResult, TaskStatus};
|
||||
|
||||
pub async fn process_task(
|
||||
task: &Task,
|
||||
task_execution_tracker: Arc<TaskExecutionTracker>,
|
||||
) -> TaskResult {
|
||||
match get_task_result(task.clone(), task_execution_tracker).await {
|
||||
Ok(data) => TaskResult {
|
||||
task_id: task.id.clone(),
|
||||
status: TaskStatus::Completed,
|
||||
data: Some(data),
|
||||
error: None,
|
||||
},
|
||||
Err(error) => TaskResult {
|
||||
task_id: task.id.clone(),
|
||||
status: TaskStatus::Failed,
|
||||
data: None,
|
||||
error: Some(error),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_task_result(
|
||||
task: Task,
|
||||
task_execution_tracker: Arc<TaskExecutionTracker>,
|
||||
) -> Result<Value, String> {
|
||||
let (command, output_identifier) = build_command(&task)?;
|
||||
let (stdout_output, stderr_output, success) = run_command(
|
||||
command,
|
||||
&output_identifier,
|
||||
&task.id,
|
||||
task_execution_tracker,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if success {
|
||||
process_output(stdout_output)
|
||||
} else {
|
||||
Err(format!("Command failed:\n{}", stderr_output))
|
||||
}
|
||||
}
|
||||
|
||||
fn build_command(task: &Task) -> Result<(Command, String), String> {
|
||||
let task_error = |field: &str| format!("Task {}: Missing {}", task.id, field);
|
||||
|
||||
let mut output_identifier = task.id.clone();
|
||||
let mut command = if task.task_type == "sub_recipe" {
|
||||
let sub_recipe_name = task
|
||||
.get_sub_recipe_name()
|
||||
.ok_or_else(|| task_error("sub_recipe name"))?;
|
||||
let path = task
|
||||
.get_sub_recipe_path()
|
||||
.ok_or_else(|| task_error("sub_recipe path"))?;
|
||||
let command_parameters = task
|
||||
.get_command_parameters()
|
||||
.ok_or_else(|| task_error("command_parameters"))?;
|
||||
|
||||
output_identifier = format!("sub-recipe {}", sub_recipe_name);
|
||||
let mut cmd = Command::new("goose");
|
||||
cmd.arg("run").arg("--recipe").arg(path).arg("--no-session");
|
||||
|
||||
for (key, value) in command_parameters {
|
||||
let key_str = key.to_string();
|
||||
let value_str = value.as_str().unwrap_or(&value.to_string()).to_string();
|
||||
cmd.arg("--params")
|
||||
.arg(format!("{}={}", key_str, value_str));
|
||||
}
|
||||
cmd
|
||||
} else {
|
||||
let text = task
|
||||
.get_text_instruction()
|
||||
.ok_or_else(|| task_error("text_instruction"))?;
|
||||
let mut cmd = Command::new("goose");
|
||||
cmd.arg("run").arg("--text").arg(text);
|
||||
cmd
|
||||
};
|
||||
|
||||
command.stdout(Stdio::piped());
|
||||
command.stderr(Stdio::piped());
|
||||
Ok((command, output_identifier))
|
||||
}
|
||||
|
||||
async fn run_command(
|
||||
mut command: Command,
|
||||
output_identifier: &str,
|
||||
task_id: &str,
|
||||
task_execution_tracker: Arc<TaskExecutionTracker>,
|
||||
) -> Result<(String, String, bool), String> {
|
||||
let mut child = command
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to spawn goose: {}", e))?;
|
||||
|
||||
let stdout = child.stdout.take().expect("Failed to capture stdout");
|
||||
let stderr = child.stderr.take().expect("Failed to capture stderr");
|
||||
|
||||
let stdout_task = spawn_output_reader(
|
||||
stdout,
|
||||
output_identifier,
|
||||
false,
|
||||
task_id,
|
||||
task_execution_tracker.clone(),
|
||||
);
|
||||
let stderr_task = spawn_output_reader(
|
||||
stderr,
|
||||
output_identifier,
|
||||
true,
|
||||
task_id,
|
||||
task_execution_tracker.clone(),
|
||||
);
|
||||
|
||||
let status = child
|
||||
.wait()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to wait for process: {}", e))?;
|
||||
|
||||
let stdout_output = stdout_task.await.unwrap();
|
||||
let stderr_output = stderr_task.await.unwrap();
|
||||
|
||||
Ok((stdout_output, stderr_output, status.success()))
|
||||
}
|
||||
|
||||
fn spawn_output_reader(
|
||||
reader: impl tokio::io::AsyncRead + Unpin + Send + 'static,
|
||||
output_identifier: &str,
|
||||
is_stderr: bool,
|
||||
task_id: &str,
|
||||
task_execution_tracker: Arc<TaskExecutionTracker>,
|
||||
) -> tokio::task::JoinHandle<String> {
|
||||
let output_identifier = output_identifier.to_string();
|
||||
let task_id = task_id.to_string();
|
||||
tokio::spawn(async move {
|
||||
let mut buffer = String::new();
|
||||
let mut lines = BufReader::new(reader).lines();
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
buffer.push_str(&line);
|
||||
buffer.push('\n');
|
||||
|
||||
if !is_stderr {
|
||||
task_execution_tracker
|
||||
.send_live_output(&task_id, &line)
|
||||
.await;
|
||||
} else {
|
||||
tracing::warn!("Task stderr [{}]: {}", output_identifier, line);
|
||||
}
|
||||
}
|
||||
buffer
|
||||
})
|
||||
}
|
||||
|
||||
fn extract_json_from_line(line: &str) -> Option<String> {
|
||||
let start = line.find('{')?;
|
||||
let end = line.rfind('}')?;
|
||||
|
||||
if start >= end {
|
||||
return None;
|
||||
}
|
||||
|
||||
let potential_json = &line[start..=end];
|
||||
if serde_json::from_str::<Value>(potential_json).is_ok() {
|
||||
Some(potential_json.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn process_output(stdout_output: String) -> Result<Value, String> {
|
||||
let last_line = stdout_output
|
||||
.lines()
|
||||
.filter(|line| !line.trim().is_empty())
|
||||
.next_back()
|
||||
.unwrap_or("");
|
||||
|
||||
if let Some(json_string) = extract_json_from_line(last_line) {
|
||||
Ok(Value::String(json_string))
|
||||
} else {
|
||||
Ok(Value::String(stdout_output))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
use crate::agents::sub_recipe_execution_tool::task_types::{SharedState, Task};
|
||||
use crate::agents::sub_recipe_execution_tool::tasks::process_task;
|
||||
use std::sync::Arc;
|
||||
|
||||
async fn receive_task(state: &SharedState) -> Option<Task> {
|
||||
let mut receiver = state.task_receiver.lock().await;
|
||||
receiver.recv().await
|
||||
}
|
||||
|
||||
pub fn spawn_worker(state: Arc<SharedState>, worker_id: usize) -> tokio::task::JoinHandle<()> {
|
||||
state.increment_active_workers();
|
||||
|
||||
tokio::spawn(async move {
|
||||
worker_loop(state, worker_id).await;
|
||||
})
|
||||
}
|
||||
|
||||
async fn worker_loop(state: Arc<SharedState>, _worker_id: usize) {
|
||||
while let Some(task) = receive_task(&state).await {
|
||||
state.task_execution_tracker.start_task(&task.id).await;
|
||||
let result = process_task(&task, state.task_execution_tracker.clone()).await;
|
||||
|
||||
if let Err(e) = state.result_sender.send(result).await {
|
||||
tracing::error!("Worker failed to send result: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
state.decrement_active_workers();
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio::time::Instant;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
const EXECUTION_STATUS_COMPLETED: &str = "completed";
|
||||
const DEFAULT_MAX_WORKERS: usize = 10;
|
||||
@@ -21,14 +22,22 @@ pub async fn execute_single_task(
|
||||
task: &Task,
|
||||
notifier: mpsc::Sender<JsonRpcMessage>,
|
||||
task_config: TaskConfig,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> ExecutionResponse {
|
||||
let start_time = Instant::now();
|
||||
let task_execution_tracker = Arc::new(TaskExecutionTracker::new(
|
||||
vec![task.clone()],
|
||||
DisplayMode::SingleTaskOutput,
|
||||
notifier,
|
||||
cancellation_token.clone(),
|
||||
));
|
||||
let result = process_task(task, task_execution_tracker.clone(), task_config).await;
|
||||
let result = process_task(
|
||||
task,
|
||||
task_execution_tracker.clone(),
|
||||
task_config,
|
||||
cancellation_token.unwrap_or_default(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Complete the task in the tracker
|
||||
task_execution_tracker
|
||||
@@ -49,11 +58,13 @@ pub async fn execute_tasks_in_parallel(
|
||||
tasks: Vec<Task>,
|
||||
notifier: Sender<JsonRpcMessage>,
|
||||
task_config: TaskConfig,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> ExecutionResponse {
|
||||
let task_execution_tracker = Arc::new(TaskExecutionTracker::new(
|
||||
tasks.clone(),
|
||||
DisplayMode::MultipleTasksOutput,
|
||||
notifier,
|
||||
cancellation_token.clone(),
|
||||
));
|
||||
let start_time = Instant::now();
|
||||
let task_count = tasks.len();
|
||||
@@ -71,7 +82,12 @@ pub async fn execute_tasks_in_parallel(
|
||||
return create_error_response(e);
|
||||
}
|
||||
|
||||
let shared_state = create_shared_state(task_rx, result_tx, task_execution_tracker.clone());
|
||||
let shared_state = create_shared_state(
|
||||
task_rx,
|
||||
result_tx,
|
||||
task_execution_tracker.clone(),
|
||||
cancellation_token.unwrap_or_default(),
|
||||
);
|
||||
|
||||
let worker_count = std::cmp::min(task_count, DEFAULT_MAX_WORKERS);
|
||||
let mut worker_handles = Vec::new();
|
||||
@@ -135,12 +151,14 @@ fn create_shared_state(
|
||||
task_rx: mpsc::Receiver<Task>,
|
||||
result_tx: mpsc::Sender<TaskResult>,
|
||||
task_execution_tracker: Arc<TaskExecutionTracker>,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Arc<SharedState> {
|
||||
Arc::new(SharedState {
|
||||
task_receiver: Arc::new(tokio::sync::Mutex::new(task_rx)),
|
||||
result_sender: result_tx,
|
||||
active_workers: Arc::new(AtomicUsize::new(0)),
|
||||
task_execution_tracker,
|
||||
cancellation_token,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::agents::subagent_task_config::TaskConfig;
|
||||
use rmcp::model::JsonRpcMessage;
|
||||
use serde_json::{json, Value};
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub async fn execute_tasks(
|
||||
input: Value,
|
||||
@@ -16,6 +17,7 @@ pub async fn execute_tasks(
|
||||
notifier: Sender<JsonRpcMessage>,
|
||||
task_config: TaskConfig,
|
||||
tasks_manager: &TasksManager,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> Result<Value, String> {
|
||||
let task_ids: Vec<String> = serde_json::from_value(
|
||||
input
|
||||
@@ -31,7 +33,8 @@ pub async fn execute_tasks(
|
||||
match execution_mode {
|
||||
ExecutionMode::Sequential => {
|
||||
if task_count == 1 {
|
||||
let response = execute_single_task(&tasks[0], notifier, task_config).await;
|
||||
let response =
|
||||
execute_single_task(&tasks[0], notifier, task_config, cancellation_token).await;
|
||||
handle_response(response)
|
||||
} else {
|
||||
Err("Sequential execution mode requires exactly one task".to_string())
|
||||
@@ -47,8 +50,13 @@ pub async fn execute_tasks(
|
||||
}
|
||||
))
|
||||
} else {
|
||||
let response: ExecutionResponse =
|
||||
execute_tasks_in_parallel(tasks, notifier.clone(), task_config).await;
|
||||
let response: ExecutionResponse = execute_tasks_in_parallel(
|
||||
tasks,
|
||||
notifier.clone(),
|
||||
task_config,
|
||||
cancellation_token,
|
||||
)
|
||||
.await;
|
||||
handle_response(response)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::agents::{
|
||||
use rmcp::model::JsonRpcMessage;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_stream;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub const SUBAGENT_EXECUTE_TASK_TOOL_NAME: &str = "subagent__execute_task";
|
||||
pub fn create_subagent_execute_task_tool() -> Tool {
|
||||
@@ -64,6 +65,7 @@ pub async fn run_tasks(
|
||||
execute_data: Value,
|
||||
task_config: TaskConfig,
|
||||
tasks_manager: &TasksManager,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> ToolCallResult {
|
||||
let (notification_tx, notification_rx) = mpsc::channel::<JsonRpcMessage>(100);
|
||||
|
||||
@@ -81,6 +83,7 @@ pub async fn run_tasks(
|
||||
notification_tx,
|
||||
task_config,
|
||||
&tasks_manager_clone,
|
||||
cancellation_token,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -3,7 +3,8 @@ use rmcp::object;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
use tokio::time::{sleep, Duration, Instant};
|
||||
use tokio::time::{Duration, Instant};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::agents::subagent_execution_tool::notification_events::{
|
||||
FailedTaskInfo, TaskCompletionStats, TaskExecutionNotificationEvent, TaskExecutionStats,
|
||||
@@ -21,7 +22,6 @@ pub enum DisplayMode {
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -62,6 +62,7 @@ pub struct TaskExecutionTracker {
|
||||
last_refresh: Arc<RwLock<Instant>>,
|
||||
notifier: mpsc::Sender<JsonRpcMessage>,
|
||||
display_mode: DisplayMode,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
}
|
||||
|
||||
impl TaskExecutionTracker {
|
||||
@@ -69,6 +70,7 @@ impl TaskExecutionTracker {
|
||||
tasks: Vec<Task>,
|
||||
display_mode: DisplayMode,
|
||||
notifier: Sender<JsonRpcMessage>,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> Self {
|
||||
let task_map = tasks
|
||||
.into_iter()
|
||||
@@ -93,6 +95,41 @@ impl TaskExecutionTracker {
|
||||
last_refresh: Arc::new(RwLock::new(Instant::now())),
|
||||
notifier,
|
||||
display_mode,
|
||||
cancellation_token,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_cancelled(&self) -> bool {
|
||||
self.cancellation_token
|
||||
.as_ref()
|
||||
.is_some_and(|t| t.is_cancelled())
|
||||
}
|
||||
|
||||
fn log_notification_error(
|
||||
&self,
|
||||
error: &mpsc::error::TrySendError<JsonRpcMessage>,
|
||||
context: &str,
|
||||
) {
|
||||
if !self.is_cancelled() {
|
||||
tracing::warn!("Failed to send {} notification: {}", context, error);
|
||||
}
|
||||
}
|
||||
|
||||
fn try_send_notification(&self, event: TaskExecutionNotificationEvent, context: &str) {
|
||||
if let Err(e) = self
|
||||
.notifier
|
||||
.try_send(JsonRpcMessage::Notification(JsonRpcNotification {
|
||||
jsonrpc: JsonRpcVersion2_0,
|
||||
notification: Notification {
|
||||
method: "notifications/message".to_string(),
|
||||
params: object!({
|
||||
"data": event.to_notification_data()
|
||||
}),
|
||||
extensions: Default::default(),
|
||||
},
|
||||
}))
|
||||
{
|
||||
self.log_notification_error(&e, context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,21 +190,7 @@ impl TaskExecutionTracker {
|
||||
formatted_line,
|
||||
);
|
||||
|
||||
if let Err(e) =
|
||||
self.notifier
|
||||
.try_send(JsonRpcMessage::Notification(JsonRpcNotification {
|
||||
jsonrpc: JsonRpcVersion2_0,
|
||||
notification: Notification {
|
||||
method: "notifications/message".to_string(),
|
||||
params: object!({
|
||||
"data": event.to_notification_data()
|
||||
}),
|
||||
extensions: Default::default(),
|
||||
},
|
||||
}))
|
||||
{
|
||||
tracing::warn!("Failed to send live output notification: {}", e);
|
||||
}
|
||||
self.try_send_notification(event, "live output");
|
||||
}
|
||||
DisplayMode::MultipleTasksOutput => {
|
||||
let mut tasks = self.tasks.write().await;
|
||||
@@ -197,6 +220,10 @@ impl TaskExecutionTracker {
|
||||
}
|
||||
|
||||
async fn send_tasks_update(&self) {
|
||||
if self.is_cancelled() {
|
||||
return;
|
||||
}
|
||||
|
||||
let tasks = self.tasks.read().await;
|
||||
let task_list: Vec<_> = tasks.values().collect();
|
||||
let (total, pending, running, completed, failed) = count_by_status(&tasks);
|
||||
@@ -229,21 +256,7 @@ impl TaskExecutionTracker {
|
||||
|
||||
let event = TaskExecutionNotificationEvent::tasks_update(stats, event_tasks);
|
||||
|
||||
if let Err(e) = self
|
||||
.notifier
|
||||
.try_send(JsonRpcMessage::Notification(JsonRpcNotification {
|
||||
jsonrpc: JsonRpcVersion2_0,
|
||||
notification: Notification {
|
||||
method: "notifications/message".to_string(),
|
||||
params: object!({
|
||||
"data": event.to_notification_data()
|
||||
}),
|
||||
extensions: Default::default(),
|
||||
},
|
||||
}))
|
||||
{
|
||||
tracing::warn!("Failed to send tasks update notification: {}", e);
|
||||
}
|
||||
self.try_send_notification(event, "tasks update");
|
||||
}
|
||||
|
||||
pub async fn refresh_display(&self) {
|
||||
@@ -276,6 +289,10 @@ impl TaskExecutionTracker {
|
||||
}
|
||||
|
||||
pub async fn send_tasks_complete(&self) {
|
||||
if self.is_cancelled() {
|
||||
return;
|
||||
}
|
||||
|
||||
let tasks = self.tasks.read().await;
|
||||
let (total, _, _, completed, failed) = count_by_status(&tasks);
|
||||
|
||||
@@ -293,23 +310,6 @@ impl TaskExecutionTracker {
|
||||
|
||||
let event = TaskExecutionNotificationEvent::tasks_complete(stats, failed_tasks);
|
||||
|
||||
if let Err(e) = self
|
||||
.notifier
|
||||
.try_send(JsonRpcMessage::Notification(JsonRpcNotification {
|
||||
jsonrpc: JsonRpcVersion2_0,
|
||||
notification: Notification {
|
||||
method: "notifications/message".to_string(),
|
||||
params: object!({
|
||||
"data": event.to_notification_data()
|
||||
}),
|
||||
extensions: Default::default(),
|
||||
},
|
||||
}))
|
||||
{
|
||||
tracing::warn!("Failed to send tasks complete notification: {}", e);
|
||||
}
|
||||
|
||||
// Brief delay to ensure completion notification is processed
|
||||
sleep(Duration::from_millis(COMPLETION_NOTIFICATION_DELAY_MS)).await;
|
||||
self.try_send_notification(event, "tasks complete");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use serde_json::{Map, Value};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::agents::subagent_execution_tool::task_execution_tracker::TaskExecutionTracker;
|
||||
|
||||
@@ -117,6 +118,7 @@ pub struct SharedState {
|
||||
pub result_sender: mpsc::Sender<TaskResult>,
|
||||
pub active_workers: Arc<AtomicUsize>,
|
||||
pub task_execution_tracker: Arc<TaskExecutionTracker>,
|
||||
pub cancellation_token: CancellationToken,
|
||||
}
|
||||
|
||||
impl SharedState {
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::process::Command;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::agents::subagent_execution_tool::task_execution_tracker::TaskExecutionTracker;
|
||||
use crate::agents::subagent_execution_tool::task_types::{Task, TaskResult, TaskStatus};
|
||||
@@ -14,8 +15,16 @@ pub async fn process_task(
|
||||
task: &Task,
|
||||
task_execution_tracker: Arc<TaskExecutionTracker>,
|
||||
task_config: TaskConfig,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> TaskResult {
|
||||
match get_task_result(task.clone(), task_execution_tracker, task_config).await {
|
||||
match get_task_result(
|
||||
task.clone(),
|
||||
task_execution_tracker,
|
||||
task_config,
|
||||
cancellation_token,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(data) => TaskResult {
|
||||
task_id: task.id.clone(),
|
||||
status: TaskStatus::Completed,
|
||||
@@ -35,10 +44,17 @@ async fn get_task_result(
|
||||
task: Task,
|
||||
task_execution_tracker: Arc<TaskExecutionTracker>,
|
||||
task_config: TaskConfig,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Result<Value, String> {
|
||||
if task.task_type == "text_instruction" {
|
||||
// Handle text_instruction tasks using subagent system
|
||||
handle_text_instruction_task(task, task_execution_tracker, task_config).await
|
||||
handle_text_instruction_task(
|
||||
task,
|
||||
task_execution_tracker,
|
||||
task_config,
|
||||
cancellation_token,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
// Handle sub_recipe tasks using command execution
|
||||
let (command, output_identifier) = build_command(&task)?;
|
||||
@@ -47,6 +63,7 @@ async fn get_task_result(
|
||||
&output_identifier,
|
||||
&task.id,
|
||||
task_execution_tracker,
|
||||
cancellation_token,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -62,6 +79,7 @@ async fn handle_text_instruction_task(
|
||||
task: Task,
|
||||
task_execution_tracker: Arc<TaskExecutionTracker>,
|
||||
task_config: TaskConfig,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Result<Value, String> {
|
||||
let text_instruction = task
|
||||
.get_text_instruction()
|
||||
@@ -76,7 +94,14 @@ async fn handle_text_instruction_task(
|
||||
// "instructions": "You are a helpful assistant. Execute the given task and provide a clear, concise response.",
|
||||
});
|
||||
|
||||
match run_complete_subagent_task(task_arguments, task_config).await {
|
||||
let result = tokio::select! {
|
||||
result = run_complete_subagent_task(task_arguments, task_config) => result,
|
||||
_ = cancellation_token.cancelled() => {
|
||||
return Err("Task cancelled".to_string());
|
||||
}
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(contents) => {
|
||||
// Extract the text content from the result
|
||||
let result_text = contents
|
||||
@@ -141,6 +166,7 @@ async fn run_command(
|
||||
output_identifier: &str,
|
||||
task_id: &str,
|
||||
task_execution_tracker: Arc<TaskExecutionTracker>,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Result<(String, String, bool), String> {
|
||||
let mut child = command
|
||||
.spawn()
|
||||
@@ -164,15 +190,25 @@ async fn run_command(
|
||||
task_execution_tracker.clone(),
|
||||
);
|
||||
|
||||
let status = child
|
||||
.wait()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to wait for process: {}", e))?;
|
||||
let result = tokio::select! {
|
||||
_ = cancellation_token.cancelled() => {
|
||||
if let Err(e) = child.kill().await {
|
||||
tracing::warn!("Failed to kill child process: {}", e);
|
||||
}
|
||||
// Abort the output reading tasks
|
||||
stdout_task.abort();
|
||||
stderr_task.abort();
|
||||
return Err("Command cancelled".to_string());
|
||||
}
|
||||
status_result = child.wait() => {
|
||||
status_result.map_err(|e| format!("Failed to wait for process: {}", e))?
|
||||
}
|
||||
};
|
||||
|
||||
let stdout_output = stdout_task.await.unwrap();
|
||||
let stderr_output = stderr_task.await.unwrap();
|
||||
|
||||
Ok((stdout_output, stderr_output, status.success()))
|
||||
Ok((stdout_output, stderr_output, result.success()))
|
||||
}
|
||||
|
||||
fn spawn_output_reader(
|
||||
|
||||
@@ -21,18 +21,35 @@ pub fn spawn_worker(
|
||||
}
|
||||
|
||||
async fn worker_loop(state: Arc<SharedState>, _worker_id: usize, task_config: TaskConfig) {
|
||||
while let Some(task) = receive_task(&state).await {
|
||||
state.task_execution_tracker.start_task(&task.id).await;
|
||||
let result = process_task(
|
||||
&task,
|
||||
state.task_execution_tracker.clone(),
|
||||
task_config.clone(),
|
||||
)
|
||||
.await;
|
||||
loop {
|
||||
tokio::select! {
|
||||
task_option = receive_task(&state) => {
|
||||
match task_option {
|
||||
Some(task) => {
|
||||
state.task_execution_tracker.start_task(&task.id).await;
|
||||
let result = process_task(
|
||||
&task,
|
||||
state.task_execution_tracker.clone(),
|
||||
task_config.clone(),
|
||||
state.cancellation_token.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Err(e) = state.result_sender.send(result).await {
|
||||
tracing::error!("Worker failed to send result: {}", e);
|
||||
break;
|
||||
if let Err(e) = state.result_sender.send(result).await {
|
||||
// Only log error if not cancelled (channel close is expected during cancellation)
|
||||
if !state.cancellation_token.is_cancelled() {
|
||||
tracing::error!("Worker failed to send result: {}", e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
None => break, // No more tasks
|
||||
}
|
||||
}
|
||||
_ = state.cancellation_token.cancelled() => {
|
||||
tracing::debug!("Worker cancelled");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use futures::stream::{self, BoxStream};
|
||||
use futures::{Stream, StreamExt};
|
||||
use rmcp::model::JsonRpcMessage;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::config::permission::PermissionLevel;
|
||||
use crate::config::PermissionManager;
|
||||
@@ -53,6 +54,7 @@ impl Agent {
|
||||
tool_futures: Arc<Mutex<Vec<(String, ToolStream)>>>,
|
||||
permission_manager: &'a mut PermissionManager,
|
||||
message_tool_response: Arc<Mutex<Message>>,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> BoxStream<'a, anyhow::Result<Message>> {
|
||||
try_stream! {
|
||||
for request in tool_requests {
|
||||
@@ -69,7 +71,7 @@ impl Agent {
|
||||
while let Some((req_id, confirmation)) = rx.recv().await {
|
||||
if req_id == request.id {
|
||||
if confirmation.permission == Permission::AllowOnce || confirmation.permission == Permission::AlwaysAllow {
|
||||
let (req_id, tool_result) = self.dispatch_tool_call(tool_call.clone(), request.id.clone()).await;
|
||||
let (req_id, tool_result) = self.dispatch_tool_call(tool_call.clone(), request.id.clone(), cancellation_token.clone()).await;
|
||||
let mut futures = tool_futures.lock().await;
|
||||
|
||||
futures.push((req_id, match tool_result {
|
||||
|
||||
@@ -603,7 +603,7 @@ mod final_output_tool_tests {
|
||||
}),
|
||||
);
|
||||
let (_, result) = agent
|
||||
.dispatch_tool_call(tool_call, "request_id".to_string())
|
||||
.dispatch_tool_call(tool_call, "request_id".to_string(), None)
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok(), "Tool call should succeed");
|
||||
|
||||
@@ -885,7 +885,7 @@ async fn test_schedule_tool_dispatch() {
|
||||
};
|
||||
|
||||
let (request_id, result) = agent
|
||||
.dispatch_tool_call(tool_call, "test_dispatch".to_string())
|
||||
.dispatch_tool_call(tool_call, "test_dispatch".to_string(), None)
|
||||
.await;
|
||||
assert_eq!(request_id, "test_dispatch");
|
||||
assert!(result.is_ok());
|
||||
|
||||
Reference in New Issue
Block a user