arqen 0.5.0

Backend infrastructure for agent-ready applications
Documentation
pub mod execution;
pub mod registry;
pub mod schema;

pub use execution::{ToolContext, ToolHandler, ToolOutcome, validate_against_schema};
pub use registry::ToolRegistry;
pub use schema::{Schema, SchemaGenerator};

/// Default HTTP path template for invoking a tool by name.
pub const AGENT_TOOL_INVOKE_PATH: &str = "/agent/tools/{name}";

use serde::{Deserialize, Serialize};

/// Metadata describing a single agent tool.
///
/// Tools are the building blocks of agent capabilities. Each tool has
/// a name, description, input/output schemas, and execution metadata.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolMetadata {
    /// Unique name of the tool (e.g., `"get_user"`, `"send_email"`).
    pub name: String,
    /// Human-readable description of what the tool does.
    pub description: String,
    /// JSON Schema describing the tool's input parameters.
    pub input: serde_json::Value,
    /// JSON Schema describing the tool's output.
    pub output: serde_json::Value,
    /// OAuth-style scopes required to invoke this tool.
    pub scopes: Vec<String>,
    /// Whether the tool reads or writes data.
    pub effect: ToolEffect,
    /// Whether invoking the tool with the same input is safe to retry.
    pub idempotent: bool,
    /// If set, the tool enqueues a job on this queue instead of executing inline.
    pub enqueues_job: Option<String>,
    /// Optional timeout in seconds for tool execution.
    pub timeout: Option<u32>,
}

/// Whether a tool reads or writes data.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ToolEffect {
    /// The tool only reads data.
    Read,
    /// The tool writes or mutates data.
    Write,
}

/// Complete manifest describing an agent application.
///
/// Generated by [`ToolRegistry::generate_manifest`] and served at
/// the `/agent/manifest` HTTP endpoint.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentManifest {
    /// Application name.
    pub name: String,
    /// Application version.
    pub version: String,
    /// Human-readable description.
    pub description: String,
    /// Storage backend mode (`"memory"`, `"persistent"`, `"http"`).
    pub storage_mode: String,
    /// Registered tools.
    pub tools: Vec<ToolMetadata>,
    /// Registered background jobs.
    pub jobs: Vec<JobMetadata>,
    /// Registered HTTP endpoints.
    pub endpoints: Vec<EndpointMetadata>,
}

/// Metadata for a background job definition.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobMetadata {
    /// Job name.
    pub name: String,
    /// Human-readable description.
    pub description: String,
    /// JSON Schema for the job payload.
    pub payload: serde_json::Value,
    /// Queue name this job is pushed to.
    pub queue: String,
    /// Maximum number of retry attempts before dead-lettering.
    pub max_retries: u32,
    /// Timeout in seconds.
    pub timeout: u32,
}

/// Metadata for an HTTP endpoint.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EndpointMetadata {
    /// URL path (e.g., `"/api/users"`).
    pub path: String,
    /// HTTP method (e.g., `"GET"`, `"POST"`).
    pub method: String,
    /// Human-readable description.
    pub description: String,
    /// Whether the endpoint requires authentication.
    pub authenticated: bool,
}

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

    fn sample_tool() -> ToolMetadata {
        ToolMetadata {
            name: "get_user".to_string(),
            description: "Get a user by ID".to_string(),
            input: serde_json::json!({"type": "object", "properties": {"id": {"type": "string"}}}),
            output: serde_json::json!({"type": "object"}),
            scopes: vec!["read:users".to_string()],
            effect: ToolEffect::Read,
            idempotent: true,
            enqueues_job: None,
            timeout: Some(30),
        }
    }

    #[test]
    fn test_tool_metadata_serialization() {
        let tool = sample_tool();
        let json = serde_json::to_value(&tool).unwrap();
        assert_eq!(json["name"], "get_user");
        assert_eq!(json["effect"], "Read");
        assert!(json["idempotent"].as_bool().unwrap());
    }

    #[test]
    fn test_tool_metadata_deserialization() {
        let json = serde_json::json!({
            "name": "create_order",
            "description": "Create an order",
            "input": {"type": "object"},
            "output": {"type": "object"},
            "scopes": ["write:orders"],
            "effect": "Write",
            "idempotent": false,
            "enqueues_job": "order_processing",
            "timeout": 60
        });
        let tool: ToolMetadata = serde_json::from_value(json).unwrap();
        assert_eq!(tool.name, "create_order");
        assert!(matches!(tool.effect, ToolEffect::Write));
        assert_eq!(tool.enqueues_job, Some("order_processing".to_string()));
    }

    #[test]
    fn test_agent_manifest_serialization() {
        let manifest = AgentManifest {
            name: "test-app".to_string(),
            version: "1.0.0".to_string(),
            description: "Test".to_string(),
            storage_mode: "memory".to_string(),
            tools: vec![sample_tool()],
            jobs: vec![],
            endpoints: vec![],
        };
        let json = serde_json::to_value(&manifest).unwrap();
        assert_eq!(json["name"], "test-app");
        assert!(json["tools"].as_array().unwrap().len() == 1);
    }

    #[test]
    fn test_job_metadata() {
        let job = JobMetadata {
            name: "send_email".to_string(),
            description: "Send an email".to_string(),
            payload: serde_json::json!({"to": "test@example.com"}),
            queue: "email_queue".to_string(),
            max_retries: 3,
            timeout: 120,
        };
        let json = serde_json::to_value(&job).unwrap();
        assert_eq!(json["queue"], "email_queue");
        assert_eq!(json["max_retries"], 3);
    }

    #[test]
    fn test_endpoint_metadata() {
        let endpoint = EndpointMetadata {
            path: "/api/users".to_string(),
            method: "GET".to_string(),
            description: "List users".to_string(),
            authenticated: true,
        };
        let json = serde_json::to_value(&endpoint).unwrap();
        assert_eq!(json["path"], "/api/users");
        assert!(json["authenticated"].as_bool().unwrap());
    }
}