use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use tokio_util::sync::CancellationToken;
use a2a_protocol_types::events::StreamResponse;
use a2a_protocol_types::message::Message;
use a2a_protocol_types::task::{TaskId, TaskState};
mod checks;
use super::report::CheckResult;
use crate::call_context::CallContext;
use crate::executor::AgentExecutor;
use crate::request_context::RequestContext;
use a2a_protocol_types::error::A2aResult;
use crate::streaming::event_queue::{
EventQueueReader, EventQueueWriter, InMemoryQueueReader, InMemoryQueueWriter,
new_in_memory_queue,
};
pub(super) struct Run {
outcome: RunOutcome,
events: Vec<StreamResponse>,
truncated: bool,
}
pub(super) enum RunOutcome {
Ok,
Err(String),
Panicked,
TimedOut,
}
pub(super) const RUN_TIMEOUT: Duration = Duration::from_secs(30);
pub(super) fn states(events: &[StreamResponse]) -> Vec<TaskState> {
events
.iter()
.filter_map(|e| match e {
StreamResponse::StatusUpdate(u) => Some(u.status.state),
_ => None,
})
.collect()
}
async fn drain(mut reader: InMemoryQueueReader) -> (Vec<StreamResponse>, bool) {
let mut events = Vec::new();
let mut truncated = false;
while let Some(item) = reader.read().await {
let Ok(event) = item else {
truncated = true;
break;
};
events.push(event.event);
}
(events, truncated)
}
struct CancelOnFirstWrite {
inner: Arc<InMemoryQueueWriter>,
token: CancellationToken,
fired: AtomicBool,
}
impl EventQueueWriter for CancelOnFirstWrite {
fn write<'a>(
&'a self,
event: StreamResponse,
) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
Box::pin(async move {
let result = self.inner.write(event).await;
if !self.fired.swap(true, Ordering::SeqCst) {
self.token.cancel();
}
result
})
}
fn close<'a>(&'a self) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
self.inner.close()
}
}
fn context(message: &Message, call_context: Option<&CallContext>) -> RequestContext {
let ctx = RequestContext::new(
message.clone(),
TaskId::new("conformance-task"),
"conformance-context".to_owned(),
);
match call_context {
Some(cc) => ctx.with_call_context(cc.clone()),
None => ctx.with_call_context(CallContext::new("conformance")),
}
}
pub(super) fn is_panic(join: &tokio::task::JoinError) -> bool {
join.is_panic()
}
impl Run {
async fn drive(
executor: &Arc<dyn AgentExecutor>,
ctx: RequestContext,
cancel_mid_run: bool,
) -> Self {
let (writer, reader) = new_in_memory_queue();
let writer = Arc::new(writer);
let exec = Arc::clone(executor);
let write_handle = Arc::clone(&writer);
let token = ctx.cancellation_token.clone();
let mut handle = if cancel_mid_run {
let wrapper = CancelOnFirstWrite {
inner: write_handle,
token,
fired: AtomicBool::new(false),
};
tokio::spawn(async move { exec.execute(&ctx, &wrapper).await })
} else {
tokio::spawn(async move { exec.execute(&ctx, write_handle.as_ref()).await })
};
let drainer = tokio::spawn(drain(reader));
let deadline = tokio::time::Instant::now() + RUN_TIMEOUT;
let outcome = match tokio::time::timeout_at(deadline, &mut handle).await {
Ok(Ok(Ok(()))) => RunOutcome::Ok,
Ok(Ok(Err(e))) => RunOutcome::Err(e.to_string()),
Ok(Err(join)) => {
if is_panic(&join) {
RunOutcome::Panicked
} else {
RunOutcome::Err(join.to_string())
}
}
Err(_) => {
handle.abort();
RunOutcome::TimedOut
}
};
drop(writer);
let (events, truncated) = match tokio::time::timeout_at(deadline, drainer).await {
Ok(Ok(collected)) => collected,
_ => (Vec::new(), true),
};
Self {
outcome,
events,
truncated,
}
}
pub(super) async fn normal(
executor: &Arc<dyn AgentExecutor>,
message: &Message,
call_context: Option<&CallContext>,
) -> Self {
Self::drive(executor, context(message, call_context), false).await
}
pub(super) async fn cancelled(
executor: &Arc<dyn AgentExecutor>,
message: &Message,
call_context: Option<&CallContext>,
) -> Self {
let ctx = context(message, call_context);
ctx.cancellation_token.cancel();
Self::drive(executor, ctx, false).await
}
pub(super) async fn cancelled_mid_run(
executor: &Arc<dyn AgentExecutor>,
message: &Message,
call_context: Option<&CallContext>,
) -> Self {
Self::drive(executor, context(message, call_context), true).await
}
}
pub(super) async fn cancel_emits_a_terminal_state(
executor: &Arc<dyn AgentExecutor>,
message: &Message,
call_context: Option<&CallContext>,
) -> CheckResult {
const NAME: &str = "cancel_emits_terminal";
let (writer, reader) = new_in_memory_queue();
let writer = Arc::new(writer);
let ctx = context(message, call_context);
let exec = Arc::clone(executor);
let write_handle = Arc::clone(&writer);
let mut handle = tokio::spawn(async move { exec.cancel(&ctx, write_handle.as_ref()).await });
let drainer = tokio::spawn(drain(reader));
let deadline = tokio::time::Instant::now() + RUN_TIMEOUT;
let Ok(joined) = tokio::time::timeout_at(deadline, &mut handle).await else {
handle.abort();
drop(writer);
let _ = tokio::time::timeout_at(deadline, drainer).await;
return CheckResult::fail(
NAME,
format!("cancel did not return within {RUN_TIMEOUT:?}"),
);
};
drop(writer);
let (events, _truncated) = match tokio::time::timeout_at(deadline, drainer).await {
Ok(Ok(collected)) => collected,
_ => (Vec::new(), true),
};
match joined {
Err(join) => {
if is_panic(&join) {
CheckResult::fail(NAME, "cancel panicked")
} else {
CheckResult::fail(NAME, format!("cancel could not be run: {join}"))
}
}
Ok(Err(e)) => CheckResult::fail(NAME, format!("cancel returned Err({e})")),
Ok(Ok(())) => {
if states(&events).iter().any(|s| s.is_terminal()) {
CheckResult::pass(NAME, "emitted a terminal status")
} else {
CheckResult::fail(
NAME,
"emitted no terminal status; a subscriber watching the stream \
is left waiting for a task that has already been cancelled",
)
}
}
}
}