use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use a2a_protocol_types::error::A2aError;
use a2a_protocol_types::events::{StreamResponse, TaskStatusUpdateEvent};
use a2a_protocol_types::failure::{FailureClass, error_class, set_class};
use a2a_protocol_types::message::{Message, MessageId, MessageRole, Part};
use a2a_protocol_types::task::{ContextId, TaskId, TaskState, TaskStatus};
use tokio::sync::OwnedSemaphorePermit;
use tokio::task::JoinHandle;
use crate::executor::AgentExecutor;
use crate::request_context::RequestContext;
use crate::streaming::{EventQueueManager, EventQueueWriter, InMemoryQueueWriter};
use super::terminal::TerminalTracking;
use super::super::{CancellationEntry, ExecutorTurn, RequestHandler};
pub(super) type CancellationTokens = Arc<tokio::sync::RwLock<HashMap<TaskId, CancellationEntry>>>;
struct CleanupGuard {
task_id: Option<TaskId>,
queue_mgr: EventQueueManager,
tokens: CancellationTokens,
turn: Arc<ExecutorTurn>,
}
impl Drop for CleanupGuard {
fn drop(&mut self) {
if let Some(tid) = self.task_id.take() {
let qmgr = self.queue_mgr.clone();
let tokens = Arc::clone(&self.tokens);
let turn = Arc::clone(&self.turn);
tokio::task::spawn(async move {
qmgr.destroy(&tid).await;
tokens.write().await.remove(&tid);
turn.finished.cancel();
});
}
}
}
impl RequestHandler {
pub(super) fn spawn_executor(
&self,
ctx: RequestContext,
writer: Arc<InMemoryQueueWriter>,
tenant_slot: Option<OwnedSemaphorePermit>,
turn: Arc<ExecutorTurn>,
) -> JoinHandle<()> {
let executor = Arc::clone(&self.executor);
let task_id = ctx.task_id.clone();
let event_queue_mgr = self.event_queue_manager.clone();
let cancel_tokens = Arc::clone(&self.cancellation_tokens);
let executor_timeout = self
.tenant_limits()
.and_then(|limits| limits.executor_timeout)
.or(self.executor_timeout);
let tenant = crate::store::tenant::TenantContext::current();
let shutdown = self.in_flight.shutdown_token();
let span_ids = (ctx.task_id.to_string(), ctx.context_id.clone());
self.in_flight
.executors()
.spawn(crate::rpc_span::in_executor_span(
&span_ids.0,
&span_ids.1,
crate::store::tenant::TenantContext::scope(tenant, async move {
let _tenant_slot = tenant_slot;
trace_debug!(task_id = %ctx.task_id, "executor started");
let mut cleanup_guard = CleanupGuard {
task_id: Some(task_id.clone()),
queue_mgr: event_queue_mgr.clone(),
tokens: Arc::clone(&cancel_tokens),
turn: Arc::clone(&turn),
};
let writer = TerminalTracking::new(writer, Arc::clone(&turn));
let result =
run_executor(executor.as_ref(), &ctx, &writer, executor_timeout).await;
if let Err((ref e, class)) = result {
write_failure_event(&writer, &ctx, e, class).await;
} else if shutdown.is_cancelled() && !writer.terminal_written() {
if let Err(_e) = executor.cancel(&ctx, &writer).await {
trace_warn!(
task_id = %ctx.task_id,
error = %_e,
"cancel hook failed during shutdown"
);
}
}
drop(writer);
event_queue_mgr.destroy(&task_id).await;
cancel_tokens.write().await.remove(&task_id);
cleanup_guard.task_id = None;
turn.finished.cancel();
}),
))
}
}
async fn run_executor(
executor: &dyn AgentExecutor,
ctx: &RequestContext,
writer: &dyn EventQueueWriter,
timeout: Option<Duration>,
) -> Result<(), (A2aError, FailureClass)> {
let Some(timeout) = timeout else {
return executor
.execute(ctx, writer)
.await
.map_err(|e| (error_class(&e), e))
.map_err(|(class, e)| (e, class));
};
match tokio::time::timeout(timeout, executor.execute(ctx, writer)).await {
Ok(Ok(())) => Ok(()),
Ok(Err(e)) => {
let class = error_class(&e);
Err((e, class))
}
Err(_) => Err((
A2aError::internal(format!("executor timed out after {}s", timeout.as_secs())),
FailureClass::BudgetExhausted,
)),
}
}
async fn write_failure_event(
writer: &dyn EventQueueWriter,
ctx: &RequestContext,
error: &A2aError,
class: FailureClass,
) {
trace_error!(task_id = %ctx.task_id, error = %error, "executor failed");
let fail_event = StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
task_id: ctx.task_id.clone(),
context_id: ContextId::new(ctx.context_id.clone()),
status: failure_status(ctx, error, class),
metadata: Some(serde_json::json!({ "error": error.to_string() })),
});
if let Err(_write_err) = writer.write(fail_event).await {
trace_error!(
task_id = %ctx.task_id,
error = %_write_err,
"failed to write failure event to queue"
);
}
}
fn failure_status(ctx: &RequestContext, error: &A2aError, class: FailureClass) -> TaskStatus {
let mut status = TaskStatus::with_timestamp(TaskState::Failed);
let mut note = Message {
id: MessageId::new(uuid::Uuid::new_v4().to_string()),
role: MessageRole::Agent,
parts: vec![Part::text(error.to_string())],
task_id: Some(ctx.task_id.clone()),
context_id: Some(ContextId::new(ctx.context_id.clone())),
reference_task_ids: None,
extensions: None,
metadata: None,
};
set_class(&mut note, class);
status.message = Some(note);
status
}