1use std::collections::VecDeque;
2
3use af_agent_session::{ContentBlock, DeliveryMode};
4
5#[derive(Debug, Clone, PartialEq)]
6pub struct InboxItem {
7 pub input_id: String,
8 pub mode: DeliveryMode,
9 pub content: Vec<ContentBlock>,
10}
11
12#[derive(Debug, Default)]
13pub struct Inbox {
14 queued: VecDeque<InboxItem>,
15}
16
17impl Inbox {
18 pub fn push(&mut self, item: InboxItem, run_active: bool) -> Result<(), InboxError> {
19 if !run_active && item.mode != DeliveryMode::Followup {
20 return Err(InboxError::NoActiveRun);
21 }
22 match item.mode {
23 DeliveryMode::Followup => self.queued.push_back(item),
24 DeliveryMode::Steer => self.queued.push_front(item),
25 DeliveryMode::Inject => {
26 while self
27 .queued
28 .front()
29 .is_some_and(|queued| queued.mode == DeliveryMode::Inject)
30 {
31 self.queued.pop_front();
32 }
33 self.queued.push_front(item);
34 }
35 }
36 Ok(())
37 }
38
39 pub fn pop(&mut self) -> Option<InboxItem> {
40 self.queued.pop_front()
41 }
42 pub fn is_empty(&self) -> bool {
43 self.queued.is_empty()
44 }
45}
46
47#[derive(Debug, thiserror::Error, PartialEq, Eq)]
48pub enum InboxError {
49 #[error("steer and inject require an active run")]
50 NoActiveRun,
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56 use af_agent_session::text;
57
58 fn item(id: &str, mode: DeliveryMode) -> InboxItem {
59 InboxItem {
60 input_id: id.into(),
61 mode,
62 content: text(id),
63 }
64 }
65
66 #[test]
67 fn inject_preempts_and_coalesces() {
68 let mut inbox = Inbox::default();
69 inbox
70 .push(item("later", DeliveryMode::Followup), true)
71 .unwrap();
72 inbox.push(item("old", DeliveryMode::Inject), true).unwrap();
73 inbox.push(item("new", DeliveryMode::Inject), true).unwrap();
74 assert_eq!(inbox.pop().unwrap().input_id, "new");
75 assert_eq!(inbox.pop().unwrap().input_id, "later");
76 }
77}