use a2a_protocol_types::error::A2aResult;
use a2a_protocol_types::events::{StreamResponse, TaskStatusUpdateEvent};
use a2a_protocol_types::task::{Task, TaskId, TaskState, TaskStatus};
use tokio_util::sync::CancellationToken;
use crate::store::TerminalStateConflict;
use crate::streaming::StreamEvent;
use crate::streaming::event_queue::carries_terminal_state;
use crate::streaming::event_queue::terminal_gate::TerminalGate;
use super::push_delivery::deliver_push_bg;
use super::record_event;
use super::state_machine::{BackgroundDeps, Outcome, process_event_with};
pub(super) struct Processor<'a> {
task_id: &'a TaskId,
deps: BackgroundDeps<'a>,
last_task: Task,
cancel: CancellationToken,
gate: Option<&'a TerminalGate>,
superseded: Option<StreamResponse>,
pushed_terminal: bool,
}
impl<'a> Processor<'a> {
pub(super) const fn new(
task_id: &'a TaskId,
deps: BackgroundDeps<'a>,
last_task: Task,
cancel: CancellationToken,
gate: Option<&'a TerminalGate>,
) -> Self {
Self {
task_id,
deps,
last_task,
cancel,
gate,
superseded: None,
pushed_terminal: false,
}
}
pub(super) async fn executor_panicked(&mut self) {
if self.superseded.is_some() || self.last_task.status.state.is_terminal() {
return;
}
self.last_task.status = TaskStatus::with_timestamp(TaskState::Failed);
if let Err(e) = self.deps.task_store.save(&self.last_task).await {
if let Some(conflict) = TerminalStateConflict::from_error(&e) {
self.supersede(&conflict, None, false).await;
return;
}
trace_error!(
task_id = %self.task_id,
error = %e,
"background processor: task store save failed after executor panic"
);
self.deps.metrics.on_persistence_error(
crate::metrics::persistence_operation::FAILED_STATE,
e.metric_label(),
);
}
}
pub(super) async fn handle(&mut self, event: A2aResult<StreamEvent>) {
let seq = event.as_ref().ok().and_then(|e| e.seq);
let terminal = event
.as_ref()
.ok()
.filter(|e| carries_terminal_state(&e.event))
.map(|e| e.event.clone());
if let Some(verdict) = &self.superseded {
if terminal.is_some() {
self.answer(seq, verdict.clone());
}
return;
}
if terminal.is_none() {
record_event(
self.deps.task_store,
self.task_id,
&event,
self.deps.metrics,
)
.await;
}
let pushes_terminal = matches!(&terminal, Some(StreamResponse::StatusUpdate(_)));
let gate = self.gate;
let release = |verdict: &StreamResponse| {
if let (Some(gate), Some(seq)) = (gate, seq) {
gate.resolve(seq, verdict.clone());
}
};
let on_persisted = || {
if let Some(event) = &terminal {
release(event);
}
};
let outcome = process_event_with(
event.map(|e| e.event),
self.task_id,
&mut self.last_task,
self.deps,
Some(&on_persisted),
)
.await;
match outcome {
Outcome::Refused(conflict) => self.supersede(&conflict, seq, terminal.is_some()).await,
Outcome::Persisted | Outcome::NotPersisted => {
if outcome == Outcome::Persisted && pushes_terminal {
self.pushed_terminal = true;
}
if let Some(event) = terminal {
release(&event);
self.record(seq, event).await;
}
}
}
}
async fn supersede(
&mut self,
conflict: &TerminalStateConflict,
seq: Option<u64>,
terminal: bool,
) {
trace_warn!(
task_id = %self.task_id,
stored = %conflict.stored,
attempted = %conflict.attempted,
"background processor: the task was finished by another writer; \
cancelling the local executor and adopting the stored state"
);
self.cancel.cancel();
match self.deps.task_store.get(self.task_id).await {
Ok(Some(stored)) if stored.status.state == conflict.stored => self.last_task = stored,
_ => self.last_task.status = TaskStatus::new(conflict.stored),
}
let verdict = StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
task_id: self.task_id.clone(),
context_id: self.last_task.context_id.clone(),
status: self.last_task.status.clone(),
metadata: None,
});
if terminal {
self.answer(seq, verdict.clone());
self.record(seq, verdict.clone()).await;
}
if !self.pushed_terminal {
deliver_push_bg(
self.task_id,
&verdict,
self.deps.push_config_store,
self.deps.push_sender,
self.deps.limits,
self.deps.metrics,
)
.await;
self.pushed_terminal = true;
}
self.superseded = Some(verdict);
}
fn answer(&self, seq: Option<u64>, verdict: StreamResponse) {
if let (Some(gate), Some(seq)) = (self.gate, seq) {
gate.resolve(seq, verdict);
}
}
async fn record(&self, seq: Option<u64>, event: StreamResponse) {
if let Some(seq) = seq {
record_event(
self.deps.task_store,
self.task_id,
&Ok(StreamEvent::at(seq, event)),
self.deps.metrics,
)
.await;
}
}
}
#[cfg(test)]
#[path = "processor_tests.rs"]
mod tests;