1use std::collections::VecDeque;
29use std::sync::atomic::{AtomicU64, Ordering};
30
31use tokio::sync::mpsc;
32use tokio_util::sync::CancellationToken;
33
34use crate::runtime::types::{LlmEvent, StreamEvent};
35
36pub const RELAY_CHANNEL_CAPACITY: usize = 64;
38pub const RELAY_DELTA_RETAINED_BUDGET_BYTES: usize = 256 * 1024;
41pub const RELAY_COALESCE_THRESHOLD_BYTES: usize = 64 * 1024;
44pub const RELAY_FORWARD_CHUNK_BYTES: usize = 16 * 1024;
47
48static PRODUCED_DELTA_BYTES: AtomicU64 = AtomicU64::new(0);
49static FORWARDED_DELTA_BYTES: AtomicU64 = AtomicU64::new(0);
50static DROPPED_DELTA_BYTES: AtomicU64 = AtomicU64::new(0);
51static RETAINED_DELTA_BYTES: AtomicU64 = AtomicU64::new(0);
52static PEAK_RETAINED_DELTA_BYTES: AtomicU64 = AtomicU64::new(0);
53static ACTIVE_RELAYS: AtomicU64 = AtomicU64::new(0);
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub struct RelaySnapshot {
60 pub produced_delta_bytes: u64,
61 pub forwarded_delta_bytes: u64,
62 pub dropped_delta_bytes: u64,
63 pub retained_delta_bytes: u64,
64 pub peak_retained_delta_bytes: u64,
65 pub active_relays: u64,
66}
67
68pub fn stream_relay_snapshot() -> RelaySnapshot {
69 RelaySnapshot {
70 produced_delta_bytes: PRODUCED_DELTA_BYTES.load(Ordering::SeqCst),
71 forwarded_delta_bytes: FORWARDED_DELTA_BYTES.load(Ordering::SeqCst),
72 dropped_delta_bytes: DROPPED_DELTA_BYTES.load(Ordering::SeqCst),
73 retained_delta_bytes: RETAINED_DELTA_BYTES.load(Ordering::SeqCst),
74 peak_retained_delta_bytes: PEAK_RETAINED_DELTA_BYTES.load(Ordering::SeqCst),
75 active_relays: ACTIVE_RELAYS.load(Ordering::SeqCst),
76 }
77}
78
79fn delta_len(event: &StreamEvent) -> Option<usize> {
81 match event {
82 StreamEvent::Llm(LlmEvent::Text(s)) | StreamEvent::Llm(LlmEvent::Thinking(s)) => {
83 Some(s.len())
84 }
85 StreamEvent::Llm(LlmEvent::ToolUseDelta { delta, .. })
86 | StreamEvent::Llm(LlmEvent::ToolResultDelta { delta, .. }) => Some(delta.len()),
87 _ => None,
88 }
89}
90
91fn try_coalesce(tail: &mut StreamEvent, incoming: &StreamEvent) -> bool {
94 match (tail, incoming) {
95 (StreamEvent::Llm(LlmEvent::Text(a)), StreamEvent::Llm(LlmEvent::Text(b))) => {
96 a.push_str(b);
97 true
98 }
99 (StreamEvent::Llm(LlmEvent::Thinking(a)), StreamEvent::Llm(LlmEvent::Thinking(b))) => {
100 a.push_str(b);
101 true
102 }
103 (
104 StreamEvent::Llm(LlmEvent::ToolUseDelta {
105 tool_id: ia,
106 delta: da,
107 }),
108 StreamEvent::Llm(LlmEvent::ToolUseDelta {
109 tool_id: ib,
110 delta: db,
111 }),
112 ) if ia == ib => {
113 da.push_str(db);
114 true
115 }
116 (
117 StreamEvent::Llm(LlmEvent::ToolResultDelta {
118 tool_id: ia,
119 delta: da,
120 }),
121 StreamEvent::Llm(LlmEvent::ToolResultDelta {
122 tool_id: ib,
123 delta: db,
124 }),
125 ) if ia == ib => {
126 da.push_str(db);
127 true
128 }
129 _ => false,
130 }
131}
132
133fn split_boundary(s: &str, n: usize) -> usize {
137 let mut i = n.min(s.len());
138 while i > 0 && !s.is_char_boundary(i) {
139 i -= 1;
140 }
141 if i == 0 && !s.is_empty() {
142 i = 1;
143 while i < s.len() && !s.is_char_boundary(i) {
144 i += 1;
145 }
146 }
147 i
148}
149
150fn trim_front(s: &mut String, want: usize) -> usize {
153 let mut cut = want.min(s.len());
154 while cut < s.len() && !s.is_char_boundary(cut) {
155 cut += 1;
156 }
157 s.drain(..cut);
158 cut
159}
160
161fn payload_mut(event: &mut StreamEvent) -> Option<&mut String> {
162 match event {
163 StreamEvent::Llm(LlmEvent::Text(s)) | StreamEvent::Llm(LlmEvent::Thinking(s)) => Some(s),
164 StreamEvent::Llm(LlmEvent::ToolUseDelta { delta, .. })
165 | StreamEvent::Llm(LlmEvent::ToolResultDelta { delta, .. }) => Some(delta),
166 _ => None,
167 }
168}
169
170fn with_payload(event: &StreamEvent, chunk: String) -> StreamEvent {
172 match event {
173 StreamEvent::Llm(LlmEvent::Text(_)) => StreamEvent::Llm(LlmEvent::Text(chunk)),
174 StreamEvent::Llm(LlmEvent::Thinking(_)) => StreamEvent::Llm(LlmEvent::Thinking(chunk)),
175 StreamEvent::Llm(LlmEvent::ToolUseDelta { tool_id, .. }) => {
176 StreamEvent::Llm(LlmEvent::ToolUseDelta {
177 tool_id: tool_id.clone(),
178 delta: chunk,
179 })
180 }
181 StreamEvent::Llm(LlmEvent::ToolResultDelta { tool_id, .. }) => {
182 StreamEvent::Llm(LlmEvent::ToolResultDelta {
183 tool_id: tool_id.clone(),
184 delta: chunk,
185 })
186 }
187 _ => unreachable!("with_payload is only called for delta events"),
188 }
189}
190
191struct RelayState {
192 pending: VecDeque<StreamEvent>,
193 retained: usize,
196}
197
198impl RelayState {
199 fn new() -> Self {
200 ACTIVE_RELAYS.fetch_add(1, Ordering::SeqCst);
201 Self {
202 pending: VecDeque::new(),
203 retained: 0,
204 }
205 }
206
207 fn ingest(&mut self, event: StreamEvent) {
208 if let Some(bytes) = delta_len(&event) {
209 PRODUCED_DELTA_BYTES.fetch_add(bytes as u64, Ordering::SeqCst);
210 self.make_room_for(bytes);
213 let mut event = event;
214 if bytes > RELAY_DELTA_RETAINED_BUDGET_BYTES {
215 let payload = payload_mut(&mut event).expect("delta payload");
217 let cut = trim_front(payload, bytes - RELAY_DELTA_RETAINED_BUDGET_BYTES);
218 DROPPED_DELTA_BYTES.fetch_add(cut as u64, Ordering::SeqCst);
219 }
220 let kept = delta_len(&event).expect("delta payload");
221 self.add_retained(kept);
222 let coalesced = self.retained > RELAY_COALESCE_THRESHOLD_BYTES
223 && self
224 .pending
225 .back_mut()
226 .is_some_and(|tail| try_coalesce(tail, &event));
227 if !coalesced {
228 self.pending.push_back(event);
229 }
230 } else {
231 self.pending.push_back(event);
232 }
233 }
234
235 fn add_retained(&mut self, bytes: usize) {
236 self.retained += bytes;
237 let now = RETAINED_DELTA_BYTES.fetch_add(bytes as u64, Ordering::SeqCst) + bytes as u64;
238 PEAK_RETAINED_DELTA_BYTES.fetch_max(now, Ordering::SeqCst);
239 }
240
241 fn sub_retained(&mut self, bytes: usize) {
242 self.retained -= bytes;
243 RETAINED_DELTA_BYTES.fetch_sub(bytes as u64, Ordering::SeqCst);
244 }
245
246 fn make_room_for(&mut self, incoming: usize) {
250 let need = incoming.min(RELAY_DELTA_RETAINED_BUDGET_BYTES);
251 while self.retained + need > RELAY_DELTA_RETAINED_BUDGET_BYTES {
252 let Some(index) = self
253 .pending
254 .iter()
255 .position(|event| delta_len(event).is_some())
256 else {
257 break;
258 };
259 let over = self.retained + need - RELAY_DELTA_RETAINED_BUDGET_BYTES;
260 let len = delta_len(&self.pending[index]).expect("droppable by position");
261 if len <= over {
262 self.pending.remove(index);
263 self.sub_retained(len);
264 DROPPED_DELTA_BYTES.fetch_add(len as u64, Ordering::SeqCst);
265 } else {
266 let payload = payload_mut(&mut self.pending[index]).expect("delta payload");
267 let cut = trim_front(payload, over);
268 self.sub_retained(cut);
269 DROPPED_DELTA_BYTES.fetch_add(cut as u64, Ordering::SeqCst);
270 }
271 }
272 }
273
274 fn next_out(&mut self) -> Option<StreamEvent> {
278 let mut event = self.pending.pop_front()?;
279 match delta_len(&event) {
280 None => Some(event),
281 Some(len) if len <= RELAY_FORWARD_CHUNK_BYTES => {
282 self.sub_retained(len);
283 FORWARDED_DELTA_BYTES.fetch_add(len as u64, Ordering::SeqCst);
284 Some(event)
285 }
286 Some(_) => {
287 let payload = payload_mut(&mut event).expect("delta payload");
288 let cut = split_boundary(payload, RELAY_FORWARD_CHUNK_BYTES);
289 let remainder = payload.split_off(cut);
290 let chunk = std::mem::take(payload);
291 if !remainder.is_empty() {
292 let rest = with_payload(&event, remainder);
293 self.pending.push_front(rest);
294 }
295 self.sub_retained(chunk.len());
296 FORWARDED_DELTA_BYTES.fetch_add(chunk.len() as u64, Ordering::SeqCst);
297 Some(with_payload(&event, chunk))
298 }
299 }
300 }
301}
302
303impl Drop for RelayState {
304 fn drop(&mut self) {
305 if self.retained > 0 {
309 DROPPED_DELTA_BYTES.fetch_add(self.retained as u64, Ordering::SeqCst);
310 RETAINED_DELTA_BYTES.fetch_sub(self.retained as u64, Ordering::SeqCst);
311 self.retained = 0;
312 }
313 ACTIVE_RELAYS.fetch_sub(1, Ordering::SeqCst);
314 }
315}
316
317pub(crate) fn spawn_bounded_stream_relay(
322 mut rx_in: mpsc::UnboundedReceiver<StreamEvent>,
323 cancel: CancellationToken,
324) -> mpsc::Receiver<StreamEvent> {
325 let (tx_out, rx_out) = mpsc::channel(RELAY_CHANNEL_CAPACITY);
326 tokio::spawn(async move {
327 let mut state = RelayState::new();
328 let mut input_open = true;
329 const DRAIN_BURST: usize = 128;
332 loop {
333 let mut drained = 0;
337 while input_open && drained < DRAIN_BURST {
338 match rx_in.try_recv() {
339 Ok(event) => {
340 state.ingest(event);
341 drained += 1;
342 }
343 Err(mpsc::error::TryRecvError::Empty) => break,
344 Err(mpsc::error::TryRecvError::Disconnected) => input_open = false,
345 }
346 }
347 while !state.pending.is_empty() {
350 match tx_out.try_reserve() {
351 Ok(permit) => {
352 if let Some(event) = state.next_out() {
353 permit.send(event);
354 }
355 }
356 Err(mpsc::error::TrySendError::Full(())) => break,
357 Err(mpsc::error::TrySendError::Closed(())) => {
358 cancel.cancel();
360 return;
361 }
362 }
363 }
364 if !input_open && state.pending.is_empty() {
366 return; }
368 if input_open {
369 if state.pending.is_empty() {
370 tokio::select! {
371 maybe = rx_in.recv() => match maybe {
372 Some(event) => state.ingest(event),
373 None => input_open = false,
374 },
375 _ = tx_out.closed() => {
378 cancel.cancel();
379 return;
380 }
381 }
382 } else {
383 tokio::select! {
384 maybe = rx_in.recv() => match maybe {
385 Some(event) => state.ingest(event),
386 None => input_open = false,
387 },
388 permit = tx_out.reserve() => match permit {
389 Ok(permit) => {
390 if let Some(event) = state.next_out() {
391 permit.send(event);
392 }
393 }
394 Err(_) => {
395 cancel.cancel();
396 return;
397 }
398 },
399 }
400 }
401 } else {
402 if let Some(event) = state.next_out() {
405 if tx_out.send(event).await.is_err() {
406 cancel.cancel();
407 return;
408 }
409 }
410 }
411 }
412 });
413 rx_out
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419 use serial_test::serial;
420 use std::time::{Duration, Instant};
421
422 const CHUNK: usize = 4096;
423
424 fn text(bytes: usize) -> StreamEvent {
425 StreamEvent::Llm(LlmEvent::Text("x".repeat(bytes)))
426 }
427
428 async fn wait_until(deadline: Duration, mut check: impl FnMut() -> bool) -> bool {
429 let end = Instant::now() + deadline;
430 while Instant::now() < end {
431 if check() {
432 return true;
433 }
434 tokio::time::sleep(Duration::from_millis(10)).await;
435 }
436 check()
437 }
438
439 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
443 #[serial(stream_relay)]
444 async fn flood_with_stalled_consumer_keeps_retention_within_fixed_budget() {
445 let base = stream_relay_snapshot();
446 let (tx, rx_in) = mpsc::unbounded_channel();
447 let cancel = CancellationToken::new();
448 let mut rx_out = spawn_bounded_stream_relay(rx_in, cancel);
449
450 const EVENTS: usize = 10_000; for _ in 0..EVENTS {
452 tx.send(text(CHUNK)).unwrap();
453 }
454 let produced_target = (EVENTS * CHUNK) as u64;
455 assert!(
456 wait_until(Duration::from_secs(20), || {
457 stream_relay_snapshot().produced_delta_bytes - base.produced_delta_bytes
458 == produced_target
459 })
460 .await,
461 "relay must ingest the full flood without caller progress"
462 );
463 let during = stream_relay_snapshot();
464 assert!(
465 during.retained_delta_bytes - base.retained_delta_bytes
466 <= RELAY_DELTA_RETAINED_BUDGET_BYTES as u64,
467 "stalled-consumer retention must stay within the fixed budget: {}",
468 during.retained_delta_bytes - base.retained_delta_bytes
469 );
470 assert!(
471 during.dropped_delta_bytes > base.dropped_delta_bytes,
472 "a flood beyond the budget must record dropped preview bytes"
473 );
474
475 drop(tx);
477 let mut delivered = 0usize;
478 while let Some(event) = rx_out.recv().await {
479 delivered += delta_len(&event).unwrap_or(0);
480 }
481 assert!(
482 delivered
483 <= RELAY_DELTA_RETAINED_BUDGET_BYTES
484 + RELAY_CHANNEL_CAPACITY * RELAY_FORWARD_CHUNK_BYTES
485 + CHUNK,
486 "late delivery must be bounded by budget + channel capacity, got {delivered}"
487 );
488 assert!(delivered > 0, "the newest preview bytes must survive");
489 assert!(
490 wait_until(Duration::from_secs(5), || {
491 stream_relay_snapshot().active_relays == base.active_relays
492 })
493 .await,
494 "relay task must terminate"
495 );
496 let end = stream_relay_snapshot();
497 assert_eq!(
498 end.produced_delta_bytes - base.produced_delta_bytes,
499 (end.forwarded_delta_bytes - base.forwarded_delta_bytes)
500 + (end.dropped_delta_bytes - base.dropped_delta_bytes),
501 "conservation: every produced preview byte is forwarded or dropped"
502 );
503 assert_eq!(end.retained_delta_bytes, base.retained_delta_bytes);
504 }
505
506 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
509 #[serial(stream_relay)]
510 async fn semantic_events_survive_flood_in_order() {
511 let (tx, rx_in) = mpsc::unbounded_channel();
512 let cancel = CancellationToken::new();
513 let mut rx_out = spawn_bounded_stream_relay(rx_in, cancel);
514
515 let semantic = [
516 StreamEvent::Llm(LlmEvent::ToolUseStart {
517 tool_name: "bash".into(),
518 tool_id: "toolu_s1".into(),
519 }),
520 StreamEvent::Llm(LlmEvent::ToolUse {
521 tool_name: "bash".into(),
522 tool_id: "toolu_s1".into(),
523 input: serde_json::json!({"command": "true"}),
524 }),
525 StreamEvent::Llm(LlmEvent::ToolResult {
526 tool_id: "toolu_s1".into(),
527 result: "ok".into(),
528 }),
529 StreamEvent::Session(crate::runtime::types::SessionEvent::Done),
530 ];
531 for event in &semantic {
532 for _ in 0..2_000 {
533 tx.send(text(CHUNK)).unwrap(); }
535 tx.send(event.clone()).unwrap();
536 }
537 drop(tx);
538
539 let mut seen = Vec::new();
540 while let Some(event) = rx_out.recv().await {
541 match &event {
542 StreamEvent::Llm(LlmEvent::ToolUseStart { tool_id, .. }) => {
543 seen.push(format!("start:{tool_id}"))
544 }
545 StreamEvent::Llm(LlmEvent::ToolUse { tool_id, .. }) => {
546 seen.push(format!("use:{tool_id}"))
547 }
548 StreamEvent::Llm(LlmEvent::ToolResult { tool_id, .. }) => {
549 seen.push(format!("result:{tool_id}"))
550 }
551 StreamEvent::Session(crate::runtime::types::SessionEvent::Done) => {
552 seen.push("done".into())
553 }
554 _ => {}
555 }
556 }
557 assert_eq!(
558 seen,
559 vec!["start:toolu_s1", "use:toolu_s1", "result:toolu_s1", "done"],
560 "every semantic event must survive the flood, in order"
561 );
562 }
563
564 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
567 #[serial(stream_relay)]
568 async fn caller_drop_cancels_turn_token_and_ends_relay() {
569 let base = stream_relay_snapshot();
570 let (tx, rx_in) = mpsc::unbounded_channel();
571 let cancel = CancellationToken::new();
572 let rx_out = spawn_bounded_stream_relay(rx_in, cancel.clone());
573 drop(rx_out);
574
575 assert!(
576 wait_until(Duration::from_secs(5), || cancel.is_cancelled()).await,
577 "caller drop must cancel the turn token"
578 );
579 assert!(
580 wait_until(Duration::from_secs(5), || {
581 stream_relay_snapshot().active_relays == base.active_relays
582 })
583 .await,
584 "relay must terminate after the caller departs"
585 );
586 drop(tx);
587 }
588
589 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
592 #[serial(stream_relay)]
593 async fn prompt_consumer_preserves_delta_granularity() {
594 let (tx, rx_in) = mpsc::unbounded_channel();
595 let cancel = CancellationToken::new();
596 let mut rx_out = spawn_bounded_stream_relay(rx_in, cancel);
597
598 for part in ["alpha ", "beta ", "gamma ", "delta ", "epsilon"] {
599 tx.send(StreamEvent::Llm(LlmEvent::Text(part.into())))
600 .unwrap();
601 }
602 drop(tx);
603 let mut parts = Vec::new();
604 while let Some(event) = rx_out.recv().await {
605 match event {
606 StreamEvent::Llm(LlmEvent::Text(s)) => parts.push(s),
607 other => panic!("unexpected event {other:?}"),
608 }
609 }
610 assert_eq!(
611 parts.concat(),
612 "alpha beta gamma delta epsilon",
613 "prompt consumers must observe complete text"
614 );
615 assert!(
616 parts.len() >= 2,
617 "small deltas below the coalesce threshold keep granularity"
618 );
619 }
620}