af-agent 0.2.0

Stable Agent model, tool, inbox, and trusted-plugin contracts.
Documentation
use std::collections::VecDeque;

use af_agent_session::{ContentBlock, DeliveryMode};

#[derive(Debug, Clone, PartialEq)]
pub struct InboxItem {
    pub input_id: String,
    pub mode: DeliveryMode,
    pub content: Vec<ContentBlock>,
}

#[derive(Debug, Default)]
pub struct Inbox {
    queued: VecDeque<InboxItem>,
}

impl Inbox {
    pub fn push(&mut self, item: InboxItem, run_active: bool) -> Result<(), InboxError> {
        if !run_active && item.mode != DeliveryMode::Followup {
            return Err(InboxError::NoActiveRun);
        }
        match item.mode {
            DeliveryMode::Followup => self.queued.push_back(item),
            DeliveryMode::Steer => self.queued.push_front(item),
            DeliveryMode::Inject => {
                while self
                    .queued
                    .front()
                    .is_some_and(|queued| queued.mode == DeliveryMode::Inject)
                {
                    self.queued.pop_front();
                }
                self.queued.push_front(item);
            }
        }
        Ok(())
    }

    pub fn pop(&mut self) -> Option<InboxItem> {
        self.queued.pop_front()
    }
    pub fn is_empty(&self) -> bool {
        self.queued.is_empty()
    }
}

#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum InboxError {
    #[error("steer and inject require an active run")]
    NoActiveRun,
}

#[cfg(test)]
mod tests {
    use super::*;
    use af_agent_session::text;

    fn item(id: &str, mode: DeliveryMode) -> InboxItem {
        InboxItem {
            input_id: id.into(),
            mode,
            content: text(id),
        }
    }

    #[test]
    fn inject_preempts_and_coalesces() {
        let mut inbox = Inbox::default();
        inbox
            .push(item("later", DeliveryMode::Followup), true)
            .unwrap();
        inbox.push(item("old", DeliveryMode::Inject), true).unwrap();
        inbox.push(item("new", DeliveryMode::Inject), true).unwrap();
        assert_eq!(inbox.pop().unwrap().input_id, "new");
        assert_eq!(inbox.pop().unwrap().input_id, "later");
    }
}