use std::sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
};
use crate::{
a2a::TaskState,
agent::{
failure_message, task_state_for, AgentLoopExit, AttemptEnding, SESSION_ENDED_STATUS_MESSAGE,
},
cancel::CANCELED_STATUS_MESSAGE,
errors::RuntimeError,
hooks::HookArtifact,
streaming::{
emit_chunk_sse, emit_chunk_sse_final, emit_sse, emit_thinking_chunk_sse, SseBroadcast,
SseEventBuffer, StreamArtifact, StreamStatus, TaskArtifactUpdateEvent,
TaskStatusUpdateEvent,
},
};
struct A2aTarget {
sse: Option<(SseBroadcast, Arc<Mutex<SseEventBuffer>>)>,
task_id: String,
context_id: Option<String>,
}
#[derive(Default)]
struct Segment {
open: bool,
streamed_text: bool,
cursor_removal_owed: bool,
streamed_thinking: bool,
}
struct Wire<'a> {
tx: &'a SseBroadcast,
buf: &'a Arc<Mutex<SseEventBuffer>>,
task_id: &'a str,
}
pub(super) struct A2aStream {
target: Option<A2aTarget>,
chunks_emitted: Arc<AtomicBool>,
segment: Segment,
result: String,
interrupted: bool,
harness_killed: bool,
spend_refusal: Option<String>,
}
impl A2aStream {
pub(super) fn new(
sse: Option<(SseBroadcast, Arc<Mutex<SseEventBuffer>>)>,
task_id: Option<String>,
context_id: Option<String>,
chunks_emitted: Arc<AtomicBool>,
) -> Self {
Self {
target: task_id.map(|task_id| A2aTarget {
sse,
task_id,
context_id,
}),
chunks_emitted,
segment: Segment::default(),
result: String::new(),
interrupted: false,
harness_killed: false,
spend_refusal: None,
}
}
pub(super) fn mark_interrupted(&mut self) {
self.interrupted = true;
}
pub(super) fn mark_harness_killed(&mut self) {
self.harness_killed = true;
}
pub(super) fn mark_spend_refused(&mut self, refusal: &crate::spend::SpendRefusal) {
self.spend_refusal = Some(refusal.to_string());
}
pub(super) async fn open_segment(&mut self, turn: u32) {
if self.segment.open {
return;
}
self.segment = Segment {
open: true,
..Segment::default()
};
self.chunks_emitted.store(false, Ordering::Relaxed);
self.status("working", &format!("inference turn {turn}"))
.await;
}
pub(super) fn text_delta(&mut self, text: &str) {
self.segment.streamed_text = true;
self.segment.cursor_removal_owed = true;
self.chunks_emitted.store(true, Ordering::Relaxed);
if let Some(wire) = self.wire() {
emit_chunk_sse(wire.tx, wire.buf, wire.task_id, text);
}
}
pub(super) fn complete_text(&mut self) {
self.pay_cursor_removal();
}
pub(super) fn thinking_delta(&mut self, text: &str) {
self.segment.streamed_thinking = true;
if let Some(wire) = self.wire() {
emit_thinking_chunk_sse(wire.tx, wire.buf, wire.task_id, text);
}
}
pub(super) fn complete_thinking(&mut self, text: &str) {
if self.segment.streamed_thinking {
return;
}
if let Some(wire) = self.wire() {
emit_thinking_chunk_sse(wire.tx, wire.buf, wire.task_id, text);
}
}
pub(super) async fn tool_result(&mut self, artifact: StreamArtifact) {
self.pay_cursor_removal();
self.segment.open = false;
self.artifact(artifact).await;
}
pub(super) async fn turn_end(&mut self, result: &str, hook_artifacts: Vec<HookArtifact>) {
self.pay_cursor_removal();
self.segment.open = false;
self.result = result.to_string();
if self.interrupted {
return;
}
for hook_artifact in hook_artifacts {
self.artifact(StreamArtifact::hook(
hook_artifact.hook_name,
hook_artifact.payload,
))
.await;
}
if self.segment.streamed_text || result.is_empty() {
return;
}
if let Some(wire) = self.wire() {
emit_chunk_sse_final(wire.tx, wire.buf, wire.task_id, result);
}
}
pub(super) fn turn_failed(&mut self) {
self.pay_cursor_removal();
self.segment.open = false;
}
pub(super) fn ending(&self, outcome: &Result<AgentLoopExit, RuntimeError>) -> AttemptEnding {
match task_state_for(outcome) {
TaskState::Completed => AttemptEnding {
state: TaskState::Completed,
message: SESSION_ENDED_STATUS_MESSAGE.to_string(),
response: Some(self.result.clone()),
},
TaskState::Canceled => AttemptEnding {
state: TaskState::Canceled,
message: canceled_message(self.harness_killed).to_string(),
response: None,
},
_ => AttemptEnding {
state: TaskState::Failed,
message: match outcome {
Ok(AgentLoopExit::SpendCeilingReached) => {
self.spend_refusal.clone().unwrap_or_default()
}
Ok(exit) => exit.as_str().to_string(),
Err(error) => failure_message(error),
},
response: None,
},
}
}
fn pay_cursor_removal(&mut self) {
if !self.segment.cursor_removal_owed {
return;
}
self.segment.cursor_removal_owed = false;
if let Some(wire) = self.wire() {
emit_chunk_sse_final(wire.tx, wire.buf, wire.task_id, "");
}
}
async fn artifact(&self, artifact: StreamArtifact) {
let Some(target) = self.target.as_ref() else {
return;
};
emit_sse(
&target.sse,
"artifact",
&TaskArtifactUpdateEvent {
id: target.task_id.clone(),
artifact,
},
)
.await;
}
async fn status(&self, state: &str, message: &str) {
let Some(target) = self.target.as_ref() else {
return;
};
emit_sse(
&target.sse,
"status",
&TaskStatusUpdateEvent {
id: target.task_id.clone(),
context_id: target.context_id.clone(),
status: StreamStatus {
state: state.to_string(),
message: message.to_string(),
response: None,
reopen: None,
},
r#final: false,
},
)
.await;
}
fn wire(&self) -> Option<Wire<'_>> {
let target = self.target.as_ref()?;
let (tx, buf) = target.sse.as_ref()?;
Some(Wire {
tx,
buf,
task_id: &target.task_id,
})
}
}
fn canceled_message(harness_killed: bool) -> &'static str {
if harness_killed {
CANCELED_KILLED_STATUS_MESSAGE
} else {
CANCELED_STATUS_MESSAGE
}
}
const CANCELED_KILLED_STATUS_MESSAGE: &str =
"task canceled; the harness was killed and its session may not resume cleanly";