agent-inbox-protocol 0.1.0

Agent inbox protocol abstracts queryable data as email.
Documentation
use chrono::{DateTime, Utc};
pub use claudius::ToolParam;
#[cfg(feature = "client")]
mod client;
#[cfg(feature = "client")]
pub use client::Client;
#[cfg(feature = "server")]
mod server;
#[cfg(feature = "server")]
pub use server::router;

macro_rules! typed_string {
    ($name:ident) => {
        impl $name {
            /// Creates a new instance if the input is valid.
            pub fn new(s: impl Into<String>) -> Option<Self> {
                let s = s.into();
                Self::validate(&s)?;
                Some(Self(s))
            }

            /// Returns the inner string as a slice.
            pub fn as_str(&self) -> &str {
                &self.0
            }
        }
    };
}

///////////////////////////////////////////// MessageID /////////////////////////////////////////////

#[derive(
    Clone,
    Debug,
    Default,
    Eq,
    PartialEq,
    Ord,
    PartialOrd,
    Hash,
    serde::Deserialize,
    serde::Serialize,
)]
pub struct MessageID(String);
typed_string!(MessageID);

impl MessageID {
    pub fn validate(_: &str) -> Option<()> {
        Some(())
    }
}

//////////////////////////////////////////// MailboxName ///////////////////////////////////////////

#[derive(
    Clone,
    Debug,
    Default,
    Eq,
    PartialEq,
    Ord,
    PartialOrd,
    Hash,
    serde::Deserialize,
    serde::Serialize,
)]
pub struct MailboxName(String);
typed_string!(MailboxName);

impl MailboxName {
    pub fn validate(_: &str) -> Option<()> {
        Some(())
    }
}

/////////////////////////////////////////////// From ///////////////////////////////////////////////

#[derive(
    Clone,
    Debug,
    Default,
    Eq,
    PartialEq,
    Ord,
    PartialOrd,
    Hash,
    serde::Deserialize,
    serde::Serialize,
)]
pub struct From(String);
typed_string!(From);

impl From {
    pub fn validate(_: &str) -> Option<()> {
        Some(())
    }
}

/////////////////////////////////////////////// Body ///////////////////////////////////////////////

#[derive(
    Clone,
    Debug,
    Default,
    Eq,
    PartialEq,
    Ord,
    PartialOrd,
    Hash,
    serde::Deserialize,
    serde::Serialize,
)]
pub struct Body(String);
typed_string!(Body);

impl Body {
    pub fn validate(_: &str) -> Option<()> {
        Some(())
    }
}

////////////////////////////////////////////// Message /////////////////////////////////////////////

#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct Message {
    pub msg_id: MessageID,
    pub date: DateTime<Utc>,
    pub from: From,
    pub body: Body,
    pub wrap: bool,
    pub tools: Vec<claudius::ToolParam>,
}

////////////////////////////////////////////// Mailbox /////////////////////////////////////////////

#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct Mailbox {
    pub name: MailboxName,
    pub messages: Vec<Message>,
}

////////////////////////////////////////// QueryParameters /////////////////////////////////////////

#[derive(Clone, Debug, Default, Eq, PartialEq, Hash, serde::Deserialize, serde::Serialize)]
pub struct QueryParameters {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub search: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub keywords: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub not_keywords: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mailbox: Option<MailboxName>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mailboxes: Option<Vec<MailboxName>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub from: Option<From>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub since: Option<DateTime<Utc>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub until: Option<DateTime<Utc>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_per_inbox: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_across_inboxes: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sort: Option<SortOrder>,
}

#[derive(Clone, Debug, Eq, PartialEq, Hash, serde::Deserialize, serde::Serialize)]
pub enum SortOrder {
    DateAsc,
    DateDesc,
    Relevance,
}

/////////////////////////////////////////// QueryResults ///////////////////////////////////////////

#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct QueryResult {
    mailboxes: Vec<Mailbox>,
}

impl QueryResult {
    /// Creates a new query result from a list of mailboxes.
    pub fn new(mailboxes: Vec<Mailbox>) -> Self {
        Self { mailboxes }
    }

    /// Returns the mailboxes in this query result.
    pub fn mailboxes(&self) -> &[Mailbox] {
        &self.mailboxes
    }

    /// Consumes the query result and returns the mailboxes.
    pub fn into_mailboxes(self) -> Vec<Mailbox> {
        self.mailboxes
    }
}

////////////////////////////////////////// MailboxProvider /////////////////////////////////////////

#[async_trait::async_trait]
pub trait MailboxProvider {
    type Error: std::error::Error;
    async fn query(&mut self, query: QueryParameters) -> Result<QueryResult, Self::Error>;
    async fn tool_call(
        &mut self,
        request: ToolCallRequest,
    ) -> Result<ToolCallResponse, Self::Error>;
}

////////////////////////////////////////// ToolCallRequest /////////////////////////////////////////

/// Request to perform a tool call on a message.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ToolCallRequest {
    /// The message ID to target.
    pub message_id: MessageID,
    /// The name of the tool to perform.
    pub name: String,
    /// The input payload for the tool, matching the tool's input_schema.
    pub input: serde_json::Value,
}

////////////////////////////////////////// ToolCallResponse ////////////////////////////////////////

/// Response from a tool call request.
#[derive(Clone, Debug, Eq, PartialEq, Hash, serde::Deserialize, serde::Serialize)]
pub struct ToolCallResponse {
    /// Whether the tool call succeeded.
    pub success: bool,
    /// Optional message describing the result.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
}

impl ToolCallResponse {
    /// Creates a successful response.
    pub fn success() -> Self {
        Self {
            success: true,
            message: None,
        }
    }

    /// Creates a successful response with a message.
    pub fn success_with_message(message: impl Into<String>) -> Self {
        Self {
            success: true,
            message: Some(message.into()),
        }
    }

    /// Creates a failure response.
    pub fn failure(message: impl Into<String>) -> Self {
        Self {
            success: false,
            message: Some(message.into()),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use claudius::ToolParam;

    #[test]
    fn message_with_tool_param_tools() {
        let tool = ToolParam {
            name: "done".to_string(),
            description: Some("Mark as done".to_string()),
            input_schema: serde_json::json!({"type": "object", "properties": {}}),
            cache_control: None,
            strict: None,
        };

        let message = Message {
            msg_id: MessageID::new("msg-1").unwrap(),
            date: Utc::now(),
            from: From::new("test@example.com").unwrap(),
            body: Body::new("Test").unwrap(),
            wrap: false,
            tools: vec![tool],
        };

        assert_eq!(message.tools.len(), 1);
        assert_eq!(message.tools[0].name, "done");
        assert_eq!(
            message.tools[0].description,
            Some("Mark as done".to_string())
        );
        // Verify input_schema is as expected.
        println!("input_schema: {:?}", message.tools[0].input_schema);
    }

    #[test]
    fn tool_call_request_with_input() {
        let request = ToolCallRequest {
            message_id: MessageID::new("msg-1").unwrap(),
            name: "defer".to_string(),
            input: serde_json::json!({"days": 3}),
        };

        assert_eq!(request.name, "defer");
        assert_eq!(request.input["days"], 3);
        // Verify input can be serialized.
        println!("request: {:?}", request);
    }
}