radkit 0.0.5

Rust AI Agent Development Kit
Documentation
//! Core tool types for LLM function calling.
//!
//! This module provides the fundamental types for representing tool declarations,
//! calls, and results in the LLM function calling protocol.
//!
//! # Overview
//!
//! - [`FunctionDeclaration`]: Describes a tool's interface (name, description, parameters schema)
//! - [`ToolCall`]: Represents an LLM's request to invoke a tool
//! - [`ToolResult`]: The outcome of executing a tool (success or error)
//! - [`ToolResponse`]: Wraps a result with the original call ID for correlation
//!
//! # Examples
//!
//! ```ignore
//! use radkit::tools::{FunctionDeclaration, ToolCall, ToolResult};
//! use serde_json::json;
//!
//! // Declare a tool
//! let declaration = FunctionDeclaration::new(
//!     "get_weather",
//!     "Get weather for a location",
//!     json!({"type": "object", "properties": {"location": {"type": "string"}}})
//! );
//!
//! // Handle a tool call
//! let call = ToolCall::new("call_123", "get_weather", json!({"location": "NYC"}));
//! let result = ToolResult::success(json!({"temp": 72}));
//! ```

use serde::{Deserialize, Serialize};
use serde_json::Value;

/// JSON Schema declaration for a callable tool function.
///
/// This describes the interface of a tool that an LLM can call, including
/// its name, description, and parameter schema. The parameters field should
/// contain a valid JSON Schema (typically an object schema with properties).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FunctionDeclaration {
    name: String,
    description: String,
    parameters: Value,
}

impl FunctionDeclaration {
    /// Creates a new function declaration.
    ///
    /// # Arguments
    ///
    /// * `name` - The function name (e.g., "`get_weather`")
    /// * `description` - Human-readable description of what the function does
    /// * `parameters` - JSON Schema describing the function parameters
    pub fn new(name: impl Into<String>, description: impl Into<String>, parameters: Value) -> Self {
        Self {
            name: name.into(),
            description: description.into(),
            parameters,
        }
    }

    /// Returns the function name.
    #[must_use]
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Returns the function description.
    #[must_use]
    pub fn description(&self) -> &str {
        &self.description
    }

    /// Returns a reference to the parameters schema.
    #[must_use]
    pub const fn parameters(&self) -> &Value {
        &self.parameters
    }

    /// Consumes the declaration and returns its parts.
    #[must_use]
    pub fn into_parts(self) -> (String, String, Value) {
        (self.name, self.description, self.parameters)
    }
}

/// Request generated by an LLM to invoke a tool.
///
/// When an LLM wants to call a tool, it generates a `ToolCall` with a unique ID,
/// the name of the tool to invoke, and the arguments as a JSON value.
///
/// `provider_metadata` is an optional opaque blob used to round-trip
/// provider-specific fields that must be echoed back in subsequent turns.
/// For example, Gemini's thinking models attach a `thought_signature` to
/// `functionCall` parts that must be preserved and re-sent verbatim.
/// All other providers leave this field `None` and it has no effect.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCall {
    id: String,
    name: String,
    arguments: Value,
    /// Provider-specific opaque metadata. Preserved and echoed back verbatim
    /// when this tool call appears in a subsequent request to the same provider.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    provider_metadata: Option<Value>,
}

impl ToolCall {
    /// Creates a new tool call.
    ///
    /// # Arguments
    ///
    /// * `id` - Unique identifier for this call (used to correlate with response)
    /// * `name` - Name of the tool to invoke
    /// * `arguments` - JSON arguments to pass to the tool
    pub fn new(id: impl Into<String>, name: impl Into<String>, arguments: Value) -> Self {
        Self {
            id: id.into(),
            name: name.into(),
            arguments,
            provider_metadata: None,
        }
    }

    /// Returns the call ID.
    #[must_use]
    pub fn id(&self) -> &str {
        &self.id
    }

    /// Returns the tool name.
    #[must_use]
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Returns a reference to the arguments.
    #[must_use]
    pub const fn arguments(&self) -> &Value {
        &self.arguments
    }

    /// Returns the opaque provider metadata, if any.
    ///
    /// This is set by providers that need to round-trip extra fields (e.g.
    /// Gemini's `thought_signature`). Callers outside providers should treat
    /// this as opaque and not rely on its structure.
    #[must_use]
    pub const fn provider_metadata(&self) -> Option<&Value> {
        self.provider_metadata.as_ref()
    }

    /// Attaches opaque provider metadata to this tool call.
    #[must_use]
    pub fn with_provider_metadata(mut self, metadata: Value) -> Self {
        self.provider_metadata = Some(metadata);
        self
    }

    /// Consumes the call and returns its parts.
    #[must_use]
    pub fn into_parts(self) -> (String, String, Value) {
        (self.id, self.name, self.arguments)
    }
}

/// Result returned by a tool execution.
///
/// Represents the outcome of executing a tool, either success with data
/// or failure with an error message. Always use the constructor methods
/// ([`ToolResult::success`], [`ToolResult::error`]) to create instances.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResult {
    success: bool,
    data: Value,
    error_message: Option<String>,
}

impl ToolResult {
    /// Creates a successful result with the given data.
    ///
    /// # Arguments
    ///
    /// * `data` - The JSON data to return from the tool
    ///
    /// # Examples
    ///
    /// ```ignore
    /// use radkit::tools::ToolResult;
    /// use serde_json::json;
    ///
    /// let result = ToolResult::success(json!({"temperature": 72}));
    /// ```
    #[must_use]
    pub const fn success(data: Value) -> Self {
        Self {
            success: true,
            data,
            error_message: None,
        }
    }

    /// Creates an error result with the given message.
    ///
    /// # Arguments
    ///
    /// * `message` - Human-readable error message
    ///
    /// # Examples
    ///
    /// ```ignore
    /// use radkit::tools::ToolResult;
    ///
    /// let result = ToolResult::error("Location not found");
    /// ```
    pub fn error(message: impl Into<String>) -> Self {
        Self {
            success: false,
            data: Value::Null,
            error_message: Some(message.into()),
        }
    }

    /// Returns true if the tool execution was successful.
    #[must_use]
    pub const fn is_success(&self) -> bool {
        self.success
    }

    /// Returns true if the tool execution failed.
    #[must_use]
    pub const fn is_error(&self) -> bool {
        !self.success
    }

    /// Returns a reference to the result data.
    #[must_use]
    pub const fn data(&self) -> &Value {
        &self.data
    }

    /// Returns the error message, if any.
    #[must_use]
    pub fn error_message(&self) -> Option<&str> {
        self.error_message.as_deref()
    }

    /// Consumes the result and returns the data.
    #[must_use]
    pub fn into_data(self) -> Value {
        self.data
    }

    /// Consumes the result and returns its parts.
    #[must_use]
    pub fn into_parts(self) -> (bool, Value, Option<String>) {
        (self.success, self.data, self.error_message)
    }
}

/// LLM-facing wrapper containing the tool execution result.
///
/// Links a [`ToolResult`] back to the original [`ToolCall`] via the call ID.
/// This allows the LLM to correlate responses with requests.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResponse {
    tool_call_id: String,
    result: ToolResult,
}

impl ToolResponse {
    /// Creates a new tool response.
    ///
    /// # Arguments
    ///
    /// * `tool_call_id` - The ID from the original `ToolCall`
    /// * `result` - The execution result
    pub fn new(tool_call_id: impl Into<String>, result: ToolResult) -> Self {
        Self {
            tool_call_id: tool_call_id.into(),
            result,
        }
    }

    /// Returns the tool call ID.
    #[must_use]
    pub fn tool_call_id(&self) -> &str {
        &self.tool_call_id
    }

    /// Returns a reference to the result.
    #[must_use]
    pub const fn result(&self) -> &ToolResult {
        &self.result
    }

    /// Consumes the response and returns the result.
    #[must_use]
    pub fn into_result(self) -> ToolResult {
        self.result
    }

    /// Consumes the response and returns its parts.
    #[must_use]
    pub fn into_parts(self) -> (String, ToolResult) {
        (self.tool_call_id, self.result)
    }
}

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

    #[test]
    fn function_declaration_accessors() {
        let declaration =
            FunctionDeclaration::new("echo", "Echo back input", json!({"type": "object"}));

        assert_eq!(declaration.name(), "echo");
        assert_eq!(declaration.description(), "Echo back input");
        assert_eq!(declaration.parameters(), &json!({"type": "object"}));

        let (name, description, _params) = declaration.into_parts();
        assert_eq!(name, "echo");
        assert_eq!(description, "Echo back input");
    }

    #[test]
    fn tool_result_success_and_error_variants() {
        let ok = ToolResult::success(json!({"value": 1}));
        assert!(ok.is_success());
        assert!(!ok.is_error());
        assert_eq!(ok.data(), &json!({"value": 1}));
        assert!(ok.error_message().is_none());

        let err = ToolResult::error("something went wrong");
        assert!(err.is_error());
        assert_eq!(err.error_message(), Some("something went wrong"));
    }

    #[test]
    fn tool_response_correlates_to_call() {
        let result = ToolResult::success(json!({}));
        let response = ToolResponse::new("call-123", result.clone());

        assert_eq!(response.tool_call_id(), "call-123");
        assert_eq!(response.result().is_success(), result.is_success());

        let (id, inner) = response.into_parts();
        assert_eq!(id, "call-123");
        assert!(inner.is_success());
    }
}