rskit-tool 0.2.0-alpha.3

Tool definition, auto-wiring, registry and middleware for agentic systems
Documentation
//! Tool definition types — MCP-aligned metadata.

use serde::{Deserialize, Serialize};

use crate::envelope::Envelope;
use crate::io::ToolSchema;

/// How the frontend should handle the tool result.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum ExecutionHint {
    /// Tool executes a real operation; result is authoritative.
    #[default]
    Backend,
    /// Tool only validates/extracts params; frontend drives the action.
    Ui,
    /// Tool executes backend AND frontend should refresh/navigate.
    Hybrid,
    /// Unknown hint from a newer protocol version; normalizes to Backend.
    #[serde(other)]
    Unknown,
}

impl Serialize for ExecutionHint {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        match self.effective() {
            Self::Backend | Self::Unknown => serializer.serialize_str("backend"),
            Self::Ui => serializer.serialize_str("ui"),
            Self::Hybrid => serializer.serialize_str("hybrid"),
        }
    }
}

impl ExecutionHint {
    /// Return the effective hint, mapping Unknown → Backend.
    #[must_use]
    pub const fn effective(self) -> Self {
        match self {
            Self::Unknown => Self::Backend,
            other => other,
        }
    }
}

/// Optional hints about tool behavior (MCP-aligned).
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Annotations {
    /// Human-readable title.
    #[serde(skip_serializing_if = "String::is_empty", default)]
    pub title: String,
    /// Grouping category for UI.
    #[serde(skip_serializing_if = "String::is_empty", default)]
    pub category: String,
    /// Freeform tags for filtering.
    #[serde(skip_serializing_if = "Vec::is_empty", default)]
    pub tags: Vec<String>,
    /// True if repeated calls produce the same result.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub idempotent_hint: Option<bool>,
    /// Tells the frontend how to handle the tool result.
    #[serde(default)]
    pub execution_hint: ExecutionHint,
}

/// Describes a tool — MCP-aligned metadata.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Definition {
    /// Unique tool identifier.
    pub name: String,
    /// Human-readable description.
    pub description: String,
    /// JSON Schema for the tool's input parameters.
    pub input_schema: ToolSchema,
    /// Optional JSON Schema for the tool's output.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub output_schema: Option<ToolSchema>,
    /// Behavioral hints that are orthogonal to the executable permission envelope.
    #[serde(default)]
    pub annotations: Annotations,
    /// Executable permission envelope — the single source of truth for what the tool may do at runtime.
    /// It carries scopes, network/filesystem/ subprocess rules, safety classification,
    /// sensitive-invocation predicates, and data-classification hints. Defaults deny network, filesystem,
    /// and subprocess access.
    #[serde(default)]
    pub envelope: Envelope,
}

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

    #[test]
    fn unknown_execution_hint_effectively_serializes_as_backend() {
        let hint: ExecutionHint = serde_json::from_str(r#""future""#).expect("unknown maps");

        assert_eq!(hint.effective(), ExecutionHint::Backend);
        assert_eq!(
            serde_json::to_value(hint).expect("hint serialises"),
            serde_json::json!("backend")
        );
    }
}