af-agent 0.4.0

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

use af_agent_session::{ContentBlock, DeliveryMode};

/// Queued input awaiting a Run.
#[derive(Debug, Clone, PartialEq)]
pub struct InboxItem {
    /// Queued input this record refers to.
    pub input_id: InputId,
    /// Execution mode this record was produced under.
    pub mode: DeliveryMode,
    /// Content blocks carried by this record.
    pub content: Vec<ContentBlock>,
}

/// Ordered pending inputs for one Session with delivery-mode rules.
#[derive(Debug, Default)]
pub struct Inbox {
    queued: VecDeque<InboxItem>,
}

impl Inbox {
    /// Queue an item; `steer` and `inject` require an active Run.
    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(())
    }

    /// Take the next item in delivery order.
    pub fn pop(&mut self) -> Option<InboxItem> {
        self.queued.pop_front()
    }
    /// Whether nothing is queued.
    pub fn is_empty(&self) -> bool {
        self.queued.is_empty()
    }
}

/// Rejected inbox operation.
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum InboxError {
    /// Steer and inject require an active run.
    #[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.parse().unwrap(),
            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");
    }
}