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