use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use a2a_protocol_types::error::A2aResult;
use a2a_protocol_types::events::StreamResponse;
use a2a_protocol_types::task::TaskState;
use super::super::ExecutorTurn;
use crate::streaming::{EventQueueWriter, InMemoryQueueWriter};
pub(super) struct TerminalTracking {
inner: Arc<InMemoryQueueWriter>,
terminal: AtomicBool,
turn: Arc<ExecutorTurn>,
}
impl TerminalTracking {
pub(super) const fn new(inner: Arc<InMemoryQueueWriter>, turn: Arc<ExecutorTurn>) -> Self {
Self {
inner,
terminal: AtomicBool::new(false),
turn,
}
}
pub(super) fn terminal_written(&self) -> bool {
self.terminal.load(Ordering::Acquire)
}
}
const fn state_of(event: &StreamResponse) -> Option<TaskState> {
match event {
StreamResponse::Task(t) => Some(t.status.state),
StreamResponse::StatusUpdate(u) => Some(u.status.state),
_ => None,
}
}
const fn is_terminal(event: &StreamResponse) -> bool {
match event {
StreamResponse::Task(t) => t.status.state.is_terminal(),
StreamResponse::StatusUpdate(u) => u.status.state.is_terminal(),
_ => false,
}
}
impl EventQueueWriter for TerminalTracking {
fn write<'a>(
&'a self,
event: StreamResponse,
) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
Box::pin(async move {
let terminal = is_terminal(&event);
if let Some(state) = state_of(&event) {
self.turn
.parked
.store(state.is_interrupted(), Ordering::Release);
}
self.inner.write(event).await?;
if terminal {
self.terminal.store(true, Ordering::Release);
}
Ok(())
})
}
#[mutants::skip]
fn close<'a>(&'a self) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
self.inner.close()
}
}
#[cfg(test)]
mod tests {
use super::*;
use a2a_protocol_types::events::TaskStatusUpdateEvent;
use a2a_protocol_types::task::{ContextId, Task, TaskId, TaskState, TaskStatus};
fn status(state: TaskState) -> StreamResponse {
StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
task_id: TaskId::new("t"),
context_id: ContextId::new("c"),
status: TaskStatus::new(state),
metadata: None,
})
}
fn snapshot(state: TaskState) -> StreamResponse {
StreamResponse::Task(Task {
id: TaskId::new("t"),
context_id: ContextId::new("c"),
status: TaskStatus::new(state),
history: None,
artifacts: None,
metadata: None,
})
}
#[tokio::test]
async fn records_a_terminal_status_or_snapshot_and_nothing_else() {
for (event, terminal) in [
(status(TaskState::Working), false),
(snapshot(TaskState::Working), false),
(status(TaskState::Canceled), true),
(snapshot(TaskState::Completed), true),
] {
let (writer, _reader) =
crate::streaming::event_queue::new_in_memory_queue_with_capacity(8);
let tracking = TerminalTracking::new(Arc::new(writer), Arc::default());
assert!(!tracking.terminal_written());
tracking.write(event.clone()).await.unwrap();
assert_eq!(tracking.terminal_written(), terminal, "{event:?}");
}
}
#[tokio::test]
async fn a_terminal_write_that_failed_is_not_recorded() {
let (writer, _reader) = crate::streaming::event_queue::new_in_memory_queue_with_options(
8,
8,
std::time::Duration::from_secs(1),
);
let tracking = TerminalTracking::new(Arc::new(writer), Arc::default());
assert!(tracking.write(status(TaskState::Completed)).await.is_err());
assert!(!tracking.terminal_written());
}
#[tokio::test]
async fn records_whether_the_latest_state_parks_the_task() {
let (writer, _reader) = crate::streaming::event_queue::new_in_memory_queue_with_capacity(8);
let turn = Arc::new(ExecutorTurn::default());
let tracking = TerminalTracking::new(Arc::new(writer), Arc::clone(&turn));
assert!(!turn.is_parked());
for (event, parked) in [
(status(TaskState::Working), false),
(status(TaskState::InputRequired), true),
(status(TaskState::Working), false),
(snapshot(TaskState::AuthRequired), true),
(snapshot(TaskState::Completed), false),
] {
tracking.write(event.clone()).await.unwrap();
assert_eq!(turn.is_parked(), parked, "{event:?}");
}
}
}