use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use a2a_protocol_types::error::{A2aError, A2aResult};
use a2a_protocol_types::events::{StreamResponse, TaskStatusUpdateEvent};
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::super::{CancellationEntry, RequestHandler};
type CancellationTokens = Arc<tokio::sync::RwLock<HashMap<TaskId, CancellationEntry>>>;
struct CleanupGuard {
task_id: Option<TaskId>,
queue_mgr: EventQueueManager,
tokens: CancellationTokens,
}
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);
tokio::task::spawn(async move {
qmgr.destroy(&tid).await;
tokens.write().await.remove(&tid);
});
}
}
}
impl RequestHandler {
pub(super) fn spawn_executor(
&self,
ctx: RequestContext,
writer: Arc<InMemoryQueueWriter>,
tenant_slot: Option<OwnedSemaphorePermit>,
) -> 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);
tokio::spawn(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),
};
let result =
run_executor(executor.as_ref(), &ctx, writer.as_ref(), executor_timeout).await;
if let Err(ref e) = result {
write_failure_event(writer.as_ref(), &ctx, e).await;
}
drop(writer);
event_queue_mgr.destroy(&task_id).await;
cancel_tokens.write().await.remove(&task_id);
cleanup_guard.task_id = None;
})
}
}
async fn run_executor(
executor: &dyn AgentExecutor,
ctx: &RequestContext,
writer: &dyn EventQueueWriter,
timeout: Option<Duration>,
) -> A2aResult<()> {
if let Some(timeout) = timeout {
tokio::time::timeout(timeout, executor.execute(ctx, writer))
.await
.unwrap_or_else(|_| {
Err(A2aError::internal(format!(
"executor timed out after {}s",
timeout.as_secs()
)))
})
} else {
executor.execute(ctx, writer).await
}
}
async fn write_failure_event(writer: &InMemoryQueueWriter, ctx: &RequestContext, error: &A2aError) {
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),
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) -> TaskStatus {
let mut status = TaskStatus::with_timestamp(TaskState::Failed);
status.message = Some(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,
});
status
}