use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{broadcast, mpsc};
use bamboo_agent_core::AgentEvent;
use bamboo_engine::events::change_feed::ChangeEvent;
use bamboo_engine::events::journal;
use actix_web::web;
use super::envelope::{feed_reset_control, terminal_control, Encoding, OutFrame, ServerEnvelope};
use crate::app_state::AppState;
use crate::handlers::agent::events::{has_running_child, Coalescer};
use crate::handlers::agent::stream::{plan_replay, ReplayPlan};
pub(crate) type OutboundTx = mpsc::Sender<OutFrame>;
async fn send_env(out: &OutboundTx, encoding: Encoding, env: ServerEnvelope) -> bool {
match env.encode(encoding) {
Some(frame) => out.send(frame).await.is_ok(),
None => true,
}
}
pub(crate) fn spawn_feed_forwarder(
out: OutboundTx,
encoding: Encoding,
mut receiver: broadcast::Receiver<Arc<ChangeEvent>>,
events_dir: PathBuf,
since: u64,
latest_at_start: u64,
) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
let ReplayPlan {
reset_from,
events,
last_replayed,
} = plan_replay(&events_dir, since, latest_at_start);
let mut last_replayed = last_replayed;
if let Some(from) = reset_from {
if !send_env(
&out,
encoding,
ServerEnvelope::control("feed", 0, feed_reset_control(from)),
)
.await
{
return;
}
}
for ce in events {
if !send_env(&out, encoding, feed_envelope(&ce)).await {
return;
}
}
loop {
match receiver.recv().await {
Ok(ce) => {
if ce.seq <= last_replayed {
continue; }
if !send_env(&out, encoding, feed_envelope(&ce)).await {
return;
}
last_replayed = ce.seq;
}
Err(broadcast::error::RecvError::Lagged(_)) => {
if let Ok(events) = journal::read_since(&events_dir, last_replayed) {
for ce in events {
if ce.seq <= last_replayed {
continue;
}
if !send_env(&out, encoding, feed_envelope(&ce)).await {
return;
}
last_replayed = ce.seq;
}
}
}
Err(broadcast::error::RecvError::Closed) => break,
}
}
})
}
fn feed_envelope(ce: &ChangeEvent) -> ServerEnvelope {
let event = serde_json::to_value(ce).unwrap_or(serde_json::Value::Null);
ServerEnvelope::event("feed", ce.seq, event)
}
#[derive(Default)]
pub(crate) struct AgentSeq(AtomicU64);
impl AgentSeq {
pub(crate) fn next(&self) -> u64 {
self.0.fetch_add(1, Ordering::Relaxed) + 1
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn spawn_agent_forwarder(
state: web::Data<AppState>,
session_id: String,
out: OutboundTx,
encoding: Encoding,
ch: String,
mut receiver: broadcast::Receiver<AgentEvent>,
budget_event_to_replay: Option<AgentEvent>,
critical_events_to_replay: Vec<AgentEvent>,
batch_ms: u64,
) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
let seq = AgentSeq::default();
for event in critical_events_to_replay {
if !emit_agent_event(&out, encoding, &ch, &seq, event).await {
return;
}
}
if let Some(event) = budget_event_to_replay {
if !emit_agent_event(&out, encoding, &ch, &seq, event).await {
return;
}
}
if batch_ms == 0 {
let mut awaiting_children = false;
loop {
match receiver.recv().await {
Ok(event) => {
let is_terminal = is_terminal_event(&event);
let is_child_completed =
matches!(event, AgentEvent::SubAgentCompleted { .. });
if !emit_agent_event(&out, encoding, &ch, &seq, event).await {
return;
}
if is_terminal {
if has_running_child(&state, &session_id).await {
awaiting_children = true;
continue;
}
let _ = send_env(
&out,
encoding,
ServerEnvelope::control(
&ch,
seq.next(),
terminal_control("complete"),
),
)
.await;
return;
}
if awaiting_children
&& is_child_completed
&& !has_running_child(&state, &session_id).await
{
let _ = send_env(
&out,
encoding,
ServerEnvelope::control(
&ch,
seq.next(),
terminal_control("complete"),
),
)
.await;
return;
}
}
Err(broadcast::error::RecvError::Lagged(_)) => continue,
Err(broadcast::error::RecvError::Closed) => return,
}
}
}
let mut coalescer = Coalescer::default();
let flush_window = Duration::from_millis(batch_ms);
let mut flush_deadline: Option<tokio::time::Instant> = None;
let mut awaiting_children = false;
loop {
let sleep_until = flush_deadline
.unwrap_or_else(|| tokio::time::Instant::now() + Duration::from_secs(86_400));
tokio::select! {
_ = tokio::time::sleep_until(sleep_until), if flush_deadline.is_some() => {
if let Some(pending) = coalescer.take_pending() {
if !emit_agent_event(&out, encoding, &ch, &seq, pending).await {
return;
}
}
flush_deadline = None;
}
recv = receiver.recv() => {
match recv {
Ok(event) => {
let is_terminal = is_terminal_event(&event);
let is_child_completed =
matches!(event, AgentEvent::SubAgentCompleted { .. });
for out_event in coalescer.push(event) {
if !emit_agent_event(&out, encoding, &ch, &seq, out_event).await {
return;
}
}
if coalescer.has_pending() {
if flush_deadline.is_none() {
flush_deadline =
Some(tokio::time::Instant::now() + flush_window);
}
} else {
flush_deadline = None;
}
if is_terminal {
if has_running_child(&state, &session_id).await {
awaiting_children = true;
continue;
}
let _ = send_env(
&out,
encoding,
ServerEnvelope::control(&ch, seq.next(), terminal_control("complete")),
)
.await;
return;
}
if awaiting_children
&& is_child_completed
&& !has_running_child(&state, &session_id).await
{
let _ = send_env(
&out,
encoding,
ServerEnvelope::control(&ch, seq.next(), terminal_control("complete")),
)
.await;
return;
}
}
Err(broadcast::error::RecvError::Lagged(_)) => {
if let Some(pending) = coalescer.take_pending() {
if !emit_agent_event(&out, encoding, &ch, &seq, pending).await {
return;
}
flush_deadline = None;
}
}
Err(broadcast::error::RecvError::Closed) => {
if let Some(pending) = coalescer.take_pending() {
let _ = emit_agent_event(&out, encoding, &ch, &seq, pending).await;
}
return;
}
}
}
}
}
})
}
async fn emit_agent_event(
out: &OutboundTx,
encoding: Encoding,
ch: &str,
seq: &AgentSeq,
event: AgentEvent,
) -> bool {
let value = match serde_json::to_value(&event) {
Ok(v) => v,
Err(_) => return true,
};
send_env(out, encoding, ServerEnvelope::event(ch, seq.next(), value)).await
}
fn is_terminal_event(event: &AgentEvent) -> bool {
matches!(
event,
AgentEvent::Complete { .. } | AgentEvent::Cancelled { .. } | AgentEvent::Error { .. }
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn agent_seq_is_monotonic_and_one_based() {
let seq = AgentSeq::default();
assert_eq!(seq.next(), 1);
assert_eq!(seq.next(), 2);
assert_eq!(seq.next(), 3);
}
#[test]
fn feed_live_skip_predicate() {
let last_replayed = 1006u64;
let skip = |seq: u64| seq <= last_replayed;
assert!(skip(1005));
assert!(skip(1006)); assert!(!skip(1007)); assert!(!skip(2000));
}
#[test]
fn coalescer_reuse_smoke() {
let mut c = Coalescer::default();
assert!(c
.push(AgentEvent::Token {
content: "Hel".into()
})
.is_empty());
assert!(c
.push(AgentEvent::Token {
content: "lo".into()
})
.is_empty());
let out = c.push(AgentEvent::Complete {
usage: Default::default(),
});
assert_eq!(out.len(), 2);
match &out[0] {
AgentEvent::Token { content } => assert_eq!(content, "Hello"),
other => panic!("expected merged Token, got {other:?}"),
}
assert!(matches!(out[1], AgentEvent::Complete { .. }));
}
#[test]
fn terminal_event_predicate() {
assert!(is_terminal_event(&AgentEvent::Complete {
usage: Default::default()
}));
assert!(is_terminal_event(&AgentEvent::Cancelled { message: None }));
assert!(is_terminal_event(&AgentEvent::Error {
message: "x".into()
}));
assert!(!is_terminal_event(&AgentEvent::Token {
content: "x".into()
}));
}
#[test]
fn feed_envelope_uses_change_event_seq_and_full_payload() {
let ce = ChangeEvent {
seq: 99,
ts: chrono::Utc::now(),
session_id: Some("s1".into()),
event: AgentEvent::Token {
content: "hi".into(),
},
};
let env = feed_envelope(&ce);
assert_eq!(env.seq, 99);
let v = serde_json::to_value(&env).unwrap();
assert_eq!(v["ch"], "feed");
assert_eq!(v["seq"], 99);
assert_eq!(v["event"]["seq"], 99);
assert_eq!(v["event"]["session_id"], "s1");
assert_eq!(v["event"]["event"]["type"], "token");
}
}