use std::{any::Any, fmt};
use serde::Serialize;
use crate::{message::ToolResultContent, tool::ToolExecutionError};
#[derive(Clone, PartialEq)]
pub struct ToolOutput {
content: Vec<ToolResultContent>,
}
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 {
pub fn text(text: impl Into<String>) -> Self {
Self::one(ToolResultContent::text(text))
}
pub fn json(value: serde_json::Value) -> Self {
Self::one(ToolResultContent::json(value))
}
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 })
}
pub fn one(content: ToolResultContent) -> Self {
Self {
content: vec![content],
}
}
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,
}
}
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,
}
}
pub fn as_content(&self) -> &[ToolResultContent] {
&self.content
}
pub fn into_content(self) -> Vec<ToolResultContent> {
self.content
}
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)
}
}
pub trait IntoToolOutput {
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> {
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>>() {
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;