1use std::{any::Any, fmt};
4
5use serde::Serialize;
6
7use crate::{OneOrMany, message::ToolResultContent, tool::ToolExecutionError};
8
9#[derive(Clone, PartialEq)]
19pub struct ToolOutput {
20 content: OneOrMany<ToolResultContent>,
21}
22
23impl fmt::Debug for ToolOutput {
24 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
25 let content_kinds = self
26 .content
27 .iter()
28 .map(|content| match content {
29 ToolResultContent::Text(_) => "text",
30 ToolResultContent::Image(_) => "image",
31 ToolResultContent::Json { .. } => "json",
32 })
33 .collect::<Vec<_>>();
34 formatter
35 .debug_struct("ToolOutput")
36 .field("content_count", &self.content.len())
37 .field("content_kinds", &content_kinds)
38 .finish()
39 }
40}
41
42impl ToolOutput {
43 pub fn text(text: impl Into<String>) -> Self {
45 Self::one(ToolResultContent::text(text))
46 }
47
48 pub fn json(value: serde_json::Value) -> Self {
53 Self::one(ToolResultContent::json(value))
54 }
55
56 pub fn content(content: OneOrMany<ToolResultContent>) -> Self {
58 Self { content }
59 }
60
61 pub fn one(content: ToolResultContent) -> Self {
63 Self::content(OneOrMany::one(content))
64 }
65
66 pub fn as_text(&self) -> Option<&str> {
68 if self.content.len() != 1 {
69 return None;
70 }
71
72 match self.content.first_ref() {
73 ToolResultContent::Text(text) if text.additional_params.is_none() => Some(&text.text),
74 ToolResultContent::Text(_)
75 | ToolResultContent::Image(_)
76 | ToolResultContent::Json { .. } => None,
77 }
78 }
79
80 pub fn as_json(&self) -> Option<&serde_json::Value> {
82 if self.content.len() != 1 {
83 return None;
84 }
85
86 match self.content.first_ref() {
87 ToolResultContent::Json { value } => Some(value),
88 ToolResultContent::Text(_) | ToolResultContent::Image(_) => None,
89 }
90 }
91
92 pub fn as_content(&self) -> &OneOrMany<ToolResultContent> {
94 &self.content
95 }
96
97 pub fn into_content(self) -> OneOrMany<ToolResultContent> {
99 self.content
100 }
101
102 pub fn render(&self) -> String {
107 if let Some(text) = self.as_text() {
108 text.to_string()
109 } else if let Some(value) = self.as_json() {
110 value.to_string()
111 } else {
112 serde_json::to_string(&self.content)
113 .unwrap_or_else(|_| "<structured tool output>".to_string())
114 }
115 }
116}
117
118impl From<String> for ToolOutput {
119 fn from(text: String) -> Self {
120 Self::text(text)
121 }
122}
123
124impl From<&str> for ToolOutput {
125 fn from(text: &str) -> Self {
126 Self::text(text)
127 }
128}
129
130impl From<serde_json::Value> for ToolOutput {
131 fn from(value: serde_json::Value) -> Self {
132 Self::json(value)
133 }
134}
135
136impl From<ToolResultContent> for ToolOutput {
137 fn from(content: ToolResultContent) -> Self {
138 Self::one(content)
139 }
140}
141
142impl From<OneOrMany<ToolResultContent>> for ToolOutput {
143 fn from(content: OneOrMany<ToolResultContent>) -> Self {
144 Self::content(content)
145 }
146}
147
148pub trait IntoToolOutput {
157 fn into_tool_output(self) -> Result<ToolOutput, ToolExecutionError>;
159}
160
161#[cfg(test)]
162mod debug_tests {
163 use crate::message::ImageMediaType;
164
165 use super::*;
166
167 #[test]
168 fn debug_reports_shape_without_tool_content() {
169 let output = ToolOutput::content(
170 OneOrMany::many(vec![
171 ToolResultContent::text("Bearer secret-tool-output"),
172 ToolResultContent::json(serde_json::json!({
173 "credential": "secret-json-output"
174 })),
175 ToolResultContent::image_base64(
176 "secret-image-output",
177 Some(ImageMediaType::PNG),
178 None,
179 ),
180 ])
181 .unwrap(),
182 );
183
184 let debug = format!("{output:?}");
185 assert!(debug.contains("content_count: 3"));
186 assert!(debug.contains("text"));
187 assert!(debug.contains("json"));
188 assert!(debug.contains("image"));
189 for secret in [
190 "secret-tool-output",
191 "secret-json-output",
192 "secret-image-output",
193 ] {
194 assert!(!debug.contains(secret));
195 }
196 }
197}
198
199impl<T> IntoToolOutput for T
200where
201 T: Serialize + 'static,
202{
203 fn into_tool_output(self) -> Result<ToolOutput, ToolExecutionError> {
204 let value = &self as &dyn Any;
211 if let Some(content) = value.downcast_ref::<ToolResultContent>() {
212 return Ok(ToolOutput::one(content.clone()));
213 }
214 if let Some(content) = value.downcast_ref::<OneOrMany<ToolResultContent>>() {
215 return Ok(ToolOutput::content(content.clone()));
216 }
217 let is_explicit_json = value.is::<serde_json::Value>();
218
219 serde_json::to_value(self)
220 .map(|value| match value {
221 serde_json::Value::String(text) if !is_explicit_json => ToolOutput::text(text),
222 value => ToolOutput::json(value),
223 })
224 .map_err(|error| {
225 ToolExecutionError::other(format!("failed to serialize tool output: {error}"))
226 .with_source(error)
227 })
228 }
229}
230
231impl IntoToolOutput for ToolOutput {
232 fn into_tool_output(self) -> Result<ToolOutput, ToolExecutionError> {
233 Ok(self)
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use crate::message::{DocumentSourceKind, ImageMediaType};
240
241 use super::*;
242
243 #[test]
244 fn json_shaped_strings_remain_literal_text() {
245 let text = r#"{"type":"image","data":"not-an-envelope"}"#.to_string();
246 let output = text.clone().into_tool_output().unwrap();
247
248 assert_eq!(output, ToolOutput::text(text.clone()));
249 let content = output.into_content();
250 assert!(matches!(content.first(), ToolResultContent::Text(value) if value.text == text));
251 }
252
253 #[test]
254 fn structured_values_remain_json_until_terminal_rendering() {
255 let value = serde_json::json!({"status": "ok", "count": 2});
256 let output = value.clone().into_tool_output().unwrap();
257
258 assert_eq!(output, ToolOutput::json(value.clone()));
259 assert_eq!(output.render(), value.to_string());
260 let content = output.into_content();
261 assert!(matches!(
262 content.first(),
263 ToolResultContent::Json { value: content_value } if content_value == value
264 ));
265 }
266
267 #[test]
268 fn explicit_json_string_is_distinct_from_literal_text() {
269 let explicit = serde_json::Value::String("hello".to_string());
270
271 let json_output = explicit.clone().into_tool_output().unwrap();
272 let text_output = "hello".to_string().into_tool_output().unwrap();
273
274 assert_eq!(json_output, ToolOutput::json(explicit.clone()));
275 assert_eq!(json_output.as_json(), Some(&explicit));
276 assert_eq!(json_output.as_text(), None);
277 assert_eq!(text_output, ToolOutput::text("hello"));
278 assert_eq!(text_output.as_text(), Some("hello"));
279 }
280
281 #[test]
282 fn explicit_image_content_preserves_its_type() {
283 let image =
284 ToolResultContent::image_base64("base64data==", Some(ImageMediaType::JPEG), None);
285 let output = image.into_tool_output().unwrap();
286
287 let content = output.into_content();
288 assert!(matches!(
289 content.first(),
290 ToolResultContent::Image(image)
291 if image.media_type == Some(ImageMediaType::JPEG)
292 && matches!(&image.data, DocumentSourceKind::Base64(data) if data == "base64data==")
293 ));
294 }
295
296 #[test]
297 fn direct_ordered_content_is_not_serialized_as_json() {
298 let content = OneOrMany::many(vec![
299 ToolResultContent::text("before"),
300 ToolResultContent::image_base64("base64data==", Some(ImageMediaType::PNG), None),
301 ToolResultContent::json(serde_json::json!({"after": true})),
302 ])
303 .unwrap();
304
305 let output = content.clone().into_tool_output().unwrap();
306
307 assert_eq!(output.as_content(), &content);
308 }
309
310 #[test]
311 fn singleton_plain_content_has_one_canonical_representation() {
312 assert_eq!(
313 ToolOutput::text("hello"),
314 ToolOutput::one(ToolResultContent::text("hello"))
315 );
316 assert_eq!(
317 ToolOutput::json(serde_json::json!({"ok": true})),
318 ToolOutput::one(ToolResultContent::json(serde_json::json!({"ok": true})))
319 );
320 }
321}