//! Agent inbox facade over the mediated input FIFO. //! //! The inbox is a bounded, per-agent view of incoming messages. It is a domain //! port so infrastructure can implement it over the existing mediated mailbox //! without creating a second competing queue. use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::ids::{AgentId, TaskId}; use crate::mailbox::TicketId; /// Default bounded inbox capacity per agent. pub const DEFAULT_AGENT_INBOX_CAPACITY: usize = 100; /// Source of an [`InboxItem`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", tag = "kind")] pub enum InboxSource { /// Human operator. Human, /// Another agent. #[serde(rename_all = "camelCase")] Agent { /// Source agent. agent_id: AgentId, }, /// A persisted background task completion. #[serde(rename_all = "camelCase")] BackgroundTask { /// Source task. task_id: TaskId, }, /// IdeA system message. System, } impl InboxSource { /// Whether this source is lossless system work that must never be dropped. #[must_use] pub const fn is_lossless_system(self) -> bool { matches!(self, Self::BackgroundTask { .. } | Self::System) } } /// Kind of inbox item. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum InboxItemKind { /// Human message. UserMessage, /// Inter-agent delegation. AgentDelegation, /// Background task completion to deliver. BackgroundCompletion, /// Session resume notice. ResumeNotice, } impl InboxItemKind { /// Whether this kind is lossless system work that must never be dropped. #[must_use] pub const fn is_lossless_system(self) -> bool { matches!(self, Self::BackgroundCompletion | Self::ResumeNotice) } } /// Message queued for an agent. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct InboxItem { /// Stable queue item id. Reuses `TicketId` because the underlying FIFO is the /// mediated mailbox. pub id: TicketId, /// Target agent. pub agent_id: AgentId, /// Message source. pub source: InboxSource, /// Message kind. pub kind: InboxItemKind, /// Message body. pub body: String, /// Creation timestamp, epoch milliseconds. pub created_at_ms: u64, /// Optional correlation id, e.g. a task id. pub correlation_id: Option, } impl InboxItem { /// Whether this item must remain persisted elsewhere rather than being lost /// on inbox overflow. #[must_use] pub const fn is_lossless_system(&self) -> bool { self.source.is_lossless_system() || self.kind.is_lossless_system() } } /// Result status of an enqueue. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum InboxReceiptStatus { /// Item was queued in the in-memory FIFO. Queued, /// Item was not queued because capacity was full, but remains durable in its /// authoritative store for later reconcile. Deferred, } /// Receipt returned by [`AgentInbox::enqueue_message`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct InboxReceipt { /// Item id. pub item_id: TicketId, /// Target agent. pub agent_id: AgentId, /// FIFO depth after the operation. pub depth: usize, /// Enqueue outcome. pub status: InboxReceiptStatus, } /// Read-only inbox snapshot. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AgentInboxSnapshot { /// Target agent. pub agent_id: AgentId, /// Number of queued inbox items. pub depth: usize, /// FIFO-ordered items. pub items: Vec, } /// Errors raised by [`AgentInbox`]. #[derive(Debug, Clone, PartialEq, Eq, Error)] pub enum InboxError { /// The bounded inbox is full. #[error("agent inbox full for {agent_id} (capacity {capacity})")] InboxFull { /// Target agent. agent_id: AgentId, /// Configured capacity. capacity: usize, }, /// The item target does not match the enqueue target. #[error("inbox item target {item_agent_id} does not match enqueue target {agent_id}")] AgentMismatch { /// Enqueue target. agent_id: AgentId, /// Item target. item_agent_id: AgentId, }, } /// Bounded per-agent inbox facade. pub trait AgentInbox: Send + Sync { /// Enqueues a message for `agent`. /// /// Normal messages fail with [`InboxError::InboxFull`] on overflow; lossless /// system/completion items return [`InboxReceiptStatus::Deferred`] instead. /// /// # Errors /// [`InboxError`] when the item is invalid or a normal message overflows. fn enqueue_message(&self, agent: AgentId, item: InboxItem) -> Result; /// Pops the next queued inbox item, if any. fn dequeue_next(&self, agent: AgentId) -> Option; /// Returns the FIFO snapshot for `agent`. fn snapshot(&self, agent: AgentId) -> AgentInboxSnapshot; }