rig-core 0.44.0

An opinionated library for building LLM powered applications.
Documentation
//! Canonical text, JSON, and multimodal tool output.
//!
//! ```
//! use rig_core::tool::ToolOutput;
//!
//! let output = ToolOutput::text("done");
//! assert_eq!(output.as_text(), Some("done"));
//! ```

use std::{any::Any, fmt};

use serde::Serialize;

use crate::{message::ToolResultContent, tool::ToolExecutionError};

/// The canonical model-visible output produced by a tool.
///
/// Every output is stored as one or more typed [`ToolResultContent`] blocks.
/// Ordinary serializable Rust values are converted through [`IntoToolOutput`]:
/// values that serialize as JSON strings become literal text blocks and all
/// other values become structured JSON blocks. An explicit
/// [`serde_json::Value`], including a JSON string, stays JSON. Multimodal tools
/// opt in explicitly with [`Self::content`]. Rig never reparses text as JSON to
/// guess whether it represents rich content.
#[derive(Clone, PartialEq)]
pub struct ToolOutput {
    content: Vec<ToolResultContent>,
}

// Serde is the content list itself; deserialization goes through
// [`ToolOutput::content`] so an empty list is rejected at the boundary, the
// same as at construction.
impl Serialize for ToolOutput {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        self.content.serialize(serializer)
    }
}

impl<'de> serde::Deserialize<'de> for ToolOutput {
    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let content = Vec::<ToolResultContent>::deserialize(deserializer)?;
        Self::content(content).map_err(serde::de::Error::custom)
    }
}

impl fmt::Debug for ToolOutput {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        let content_kinds = self
            .content
            .iter()
            .map(|content| match content {
                ToolResultContent::Text(_) => "text",
                ToolResultContent::Image(_) => "image",
                ToolResultContent::Json { .. } => "json",
            })
            .collect::<Vec<_>>();
        formatter
            .debug_struct("ToolOutput")
            .field("content_count", &self.content.len())
            .field("content_kinds", &content_kinds)
            .finish()
    }
}

impl ToolOutput {
    /// Construct literal text output.
    pub fn text(text: impl Into<String>) -> Self {
        Self::one(ToolResultContent::text(text))
    }

    /// Construct structured JSON output.
    ///
    /// Unlike an ordinary Rust string tool output, an explicit JSON string stays
    /// a JSON content block.
    pub fn json(value: serde_json::Value) -> Self {
        Self::one(ToolResultContent::json(value))
    }

    /// Constructs explicit model content, rejecting an empty block list.
    /// Use [`Self::text`] with `""` to represent an empty text result.
    pub fn content(content: Vec<ToolResultContent>) -> Result<Self, ToolExecutionError> {
        if content.is_empty() {
            return Err(ToolExecutionError::other(
                "tool output has no content blocks; return at least one block — \
                 an empty text block is valid",
            ));
        }
        Ok(Self { content })
    }

    /// Construct one explicit model-content block.
    pub fn one(content: ToolResultContent) -> Self {
        Self {
            content: vec![content],
        }
    }

    /// Return literal text when this output is exactly one plain text block.
    pub fn as_text(&self) -> Option<&str> {
        if self.content.len() != 1 {
            return None;
        }

        match self.content.first()? {
            ToolResultContent::Text(text) if text.native.is_none() => Some(&text.text),
            ToolResultContent::Text(_)
            | ToolResultContent::Image(_)
            | ToolResultContent::Json { .. } => None,
        }
    }

    /// Return structured JSON when this output is exactly one JSON block.
    pub fn as_json(&self) -> Option<&serde_json::Value> {
        if self.content.len() != 1 {
            return None;
        }

        match self.content.first()? {
            ToolResultContent::Json { value } => Some(value),
            ToolResultContent::Text(_) | ToolResultContent::Image(_) => None,
        }
    }

    /// Borrow the canonical ordered content blocks.
    pub fn as_content(&self) -> &[ToolResultContent] {
        &self.content
    }

    /// Convert this output into the canonical message content sent to a model.
    pub fn into_content(self) -> Vec<ToolResultContent> {
        self.content
    }

    /// Render a stable text representation for telemetry and diagnostics.
    ///
    /// This is a terminal rendering operation; the returned text is never used
    /// to reconstruct structured output.
    pub fn render(&self) -> String {
        if let Some(text) = self.as_text() {
            text.to_string()
        } else if let Some(value) = self.as_json() {
            value.to_string()
        } else {
            serde_json::to_string(&self.content)
                .unwrap_or_else(|_| "<structured tool output>".to_string())
        }
    }
}

impl From<String> for ToolOutput {
    fn from(text: String) -> Self {
        Self::text(text)
    }
}

impl From<&str> for ToolOutput {
    fn from(text: &str) -> Self {
        Self::text(text)
    }
}

impl From<serde_json::Value> for ToolOutput {
    fn from(value: serde_json::Value) -> Self {
        Self::json(value)
    }
}

impl From<ToolResultContent> for ToolOutput {
    fn from(content: ToolResultContent) -> Self {
        Self::one(content)
    }
}

impl TryFrom<Vec<ToolResultContent>> for ToolOutput {
    type Error = ToolExecutionError;

    fn try_from(content: Vec<ToolResultContent>) -> Result<Self, Self::Error> {
        Self::content(content)
    }
}

/// Conversion into Rig's canonical tool output.
///
/// A blanket implementation keeps ordinary [`Serialize`] outputs ergonomic.
/// Because that blanket implementation already covers every serializable type,
/// it cannot be overridden with another implementation for a serializable
/// custom type. Return [`ToolOutput`] from [`PortableTool::call`](crate::tool::PortableTool::call)
/// when that type needs a custom presentation. Implement this trait directly
/// only for output types that do not implement [`Serialize`].
pub trait IntoToolOutput {
    /// Convert this value without routing structured data through a string.
    fn into_tool_output(self) -> Result<ToolOutput, ToolExecutionError>;
}

#[cfg(test)]
mod debug_tests;

impl<T> IntoToolOutput for T
where
    T: Serialize + 'static,
{
    fn into_tool_output(self) -> Result<ToolOutput, ToolExecutionError> {
        // Preserve explicit content types before the serialization fallback so
        // multimodal blocks are not converted into JSON objects.
        let value = &self as &dyn Any;
        if let Some(output) = value.downcast_ref::<ToolOutput>() {
            return Ok(output.clone());
        }
        if let Some(content) = value.downcast_ref::<ToolResultContent>() {
            return Ok(ToolOutput::one(content.clone()));
        }
        if let Some(content) = value.downcast_ref::<Vec<ToolResultContent>>() {
            // Reject empty rich output as a tool failure rather than inventing
            // text or allowing an invalid result into history.
            return ToolOutput::content(content.clone());
        }
        let is_explicit_json = value.is::<serde_json::Value>();

        serde_json::to_value(self)
            .map(|value| match value {
                serde_json::Value::String(text) if !is_explicit_json => ToolOutput::text(text),
                value => ToolOutput::json(value),
            })
            .map_err(|error| {
                ToolExecutionError::other(format!("failed to serialize tool output: {error}"))
                    .with_source(error)
            })
    }
}

#[cfg(test)]
mod tests;