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