Skip to main content

lash_sansio/
tool_output.rs

1use std::collections::BTreeMap;
2
3use serde::de::{Error as DeError, MapAccess, Visitor};
4use serde::ser::SerializeMap;
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6use serde_json::{Map, Number, Value};
7
8use crate::llm::types::AttachmentSource;
9
10const TAG_KEY: &str = "$lash_tool_value";
11const ATTACHMENT_TAG: &str = "attachment";
12const OBJECT_TAG: &str = "object";
13const SOURCE_KEY: &str = "source";
14const ENTRIES_KEY: &str = "entries";
15
16#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
17pub struct ToolCallOutput {
18    pub outcome: ToolCallOutcome,
19    #[serde(default, skip_serializing_if = "Option::is_none")]
20    pub control: Option<ToolControl>,
21}
22
23#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
24pub struct ToolCallRecord {
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub call_id: Option<String>,
27    pub tool: String,
28    pub args: Value,
29    pub output: ToolCallOutput,
30    pub duration_ms: u64,
31}
32
33impl ToolCallOutput {
34    pub fn success(value: impl Into<ToolValue>) -> Self {
35        Self {
36            outcome: ToolCallOutcome::Success(value.into()),
37            control: None,
38        }
39    }
40
41    pub fn failure(failure: ToolFailure) -> Self {
42        Self {
43            outcome: ToolCallOutcome::Failure(failure),
44            control: None,
45        }
46    }
47
48    pub fn cancelled(cancellation: ToolCancellation) -> Self {
49        Self {
50            outcome: ToolCallOutcome::Cancelled(cancellation),
51            control: None,
52        }
53    }
54
55    pub fn with_control(mut self, control: ToolControl) -> Self {
56        self.control = Some(control);
57        self
58    }
59
60    pub fn is_success(&self) -> bool {
61        matches!(self.outcome, ToolCallOutcome::Success(_))
62    }
63
64    pub fn status(&self) -> ToolCallStatus {
65        match self.outcome {
66            ToolCallOutcome::Success(_) => ToolCallStatus::Success,
67            ToolCallOutcome::Failure(_) => ToolCallStatus::Failure,
68            ToolCallOutcome::Cancelled(_) => ToolCallStatus::Cancelled,
69        }
70    }
71
72    pub fn value_for_projection(&self) -> Value {
73        match &self.outcome {
74            ToolCallOutcome::Success(value) => value.to_json_value(),
75            ToolCallOutcome::Failure(failure) => failure.to_json_value(),
76            ToolCallOutcome::Cancelled(cancellation) => cancellation.to_json_value(),
77        }
78    }
79
80    pub fn into_value_for_projection(self) -> Value {
81        match self.outcome {
82            ToolCallOutcome::Success(value) => value.into_json_value(),
83            ToolCallOutcome::Failure(failure) => failure.to_json_value(),
84            ToolCallOutcome::Cancelled(cancellation) => cancellation.to_json_value(),
85        }
86    }
87
88    pub fn attachments(&self) -> Vec<AttachmentSource> {
89        match &self.outcome {
90            ToolCallOutcome::Success(value) => value.attachments(),
91            ToolCallOutcome::Failure(failure) => failure
92                .raw
93                .as_ref()
94                .map(ToolValue::attachments)
95                .unwrap_or_default(),
96            ToolCallOutcome::Cancelled(cancellation) => cancellation
97                .raw
98                .as_ref()
99                .map(ToolValue::attachments)
100                .unwrap_or_default(),
101        }
102    }
103
104    pub fn replace_attachment_source(
105        &mut self,
106        previous: &AttachmentSource,
107        replacement: &AttachmentSource,
108    ) {
109        match &mut self.outcome {
110            ToolCallOutcome::Success(value) => {
111                value.replace_attachment_source(previous, replacement)
112            }
113            ToolCallOutcome::Failure(failure) => {
114                if let Some(raw) = failure.raw.as_mut() {
115                    raw.replace_attachment_source(previous, replacement);
116                }
117            }
118            ToolCallOutcome::Cancelled(cancellation) => {
119                if let Some(raw) = cancellation.raw.as_mut() {
120                    raw.replace_attachment_source(previous, replacement);
121                }
122            }
123        }
124    }
125}
126
127pub fn format_tool_output_content(output: &ToolCallOutput) -> String {
128    match &output.outcome {
129        ToolCallOutcome::Success(value) => {
130            let value = value.to_json_value();
131            match value {
132                Value::String(text) => text,
133                other => serde_json::to_string(&other).unwrap_or_else(|_| "null".to_string()),
134            }
135        }
136        ToolCallOutcome::Failure(failure) => format_failure_message(failure),
137        ToolCallOutcome::Cancelled(cancellation) => format_cancellation_message(cancellation),
138    }
139}
140
141#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
142#[serde(rename_all = "snake_case")]
143pub enum ToolCallStatus {
144    Success,
145    Failure,
146    Cancelled,
147}
148
149#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
150#[serde(tag = "status", content = "payload", rename_all = "snake_case")]
151pub enum ToolCallOutcome {
152    Success(ToolValue),
153    Failure(ToolFailure),
154    Cancelled(ToolCancellation),
155}
156
157#[derive(Clone, Debug, PartialEq)]
158pub enum ToolValue {
159    Null,
160    Bool(bool),
161    Number(Number),
162    String(String),
163    Array(Vec<ToolValue>),
164    Object(BTreeMap<String, ToolValue>),
165    Attachment(AttachmentSource),
166}
167
168impl ToolValue {
169    pub fn to_json_value(&self) -> Value {
170        match self {
171            Self::Null => Value::Null,
172            Self::Bool(value) => Value::Bool(*value),
173            Self::Number(value) => Value::Number(value.clone()),
174            Self::String(value) => Value::String(value.clone()),
175            Self::Array(values) => Value::Array(values.iter().map(Self::to_json_value).collect()),
176            Self::Attachment(source) => tagged_attachment_json(source),
177            Self::Object(entries) => object_tool_value_to_json(entries),
178        }
179    }
180
181    pub fn into_json_value(self) -> Value {
182        match self {
183            Self::Null => Value::Null,
184            Self::Bool(value) => Value::Bool(value),
185            Self::Number(value) => Value::Number(value),
186            Self::String(value) => Value::String(value),
187            Self::Array(values) => {
188                Value::Array(values.into_iter().map(Self::into_json_value).collect())
189            }
190            Self::Attachment(source) => tagged_attachment_json(&source),
191            Self::Object(entries) => object_tool_value_into_json(entries),
192        }
193    }
194
195    pub fn from_json_value(value: Value) -> serde_json::Result<Self> {
196        serde_json::from_value(value)
197    }
198
199    pub fn attachments(&self) -> Vec<AttachmentSource> {
200        let mut attachments = Vec::new();
201        self.collect_attachments(&mut attachments);
202        attachments
203    }
204
205    pub fn model_parts(&self) -> Vec<ModelToolReturnPart> {
206        let mut parts = Vec::new();
207        match self {
208            Self::String(text) => push_text_part(&mut parts, text.clone()),
209            Self::Attachment(reference) => {
210                parts.push(ModelToolReturnPart::Attachment(reference.clone()))
211            }
212            Self::Null | Self::Bool(_) | Self::Number(_) | Self::Array(_) | Self::Object(_) => {
213                self.push_compact_model_parts(&mut parts);
214            }
215        }
216        parts
217    }
218
219    fn collect_attachments(&self, attachments: &mut Vec<AttachmentSource>) {
220        match self {
221            Self::Attachment(reference) => attachments.push(reference.clone()),
222            Self::Array(values) => {
223                for value in values {
224                    value.collect_attachments(attachments);
225                }
226            }
227            Self::Object(entries) => {
228                for value in entries.values() {
229                    value.collect_attachments(attachments);
230                }
231            }
232            Self::Null | Self::Bool(_) | Self::Number(_) | Self::String(_) => {}
233        }
234    }
235
236    fn replace_attachment_source(
237        &mut self,
238        previous: &AttachmentSource,
239        replacement: &AttachmentSource,
240    ) {
241        match self {
242            Self::Attachment(source) if source == previous => *source = replacement.clone(),
243            Self::Array(values) => {
244                for value in values {
245                    value.replace_attachment_source(previous, replacement);
246                }
247            }
248            Self::Object(entries) => {
249                for value in entries.values_mut() {
250                    value.replace_attachment_source(previous, replacement);
251                }
252            }
253            Self::Null
254            | Self::Bool(_)
255            | Self::Number(_)
256            | Self::String(_)
257            | Self::Attachment(_) => {}
258        }
259    }
260
261    fn push_compact_model_parts(&self, parts: &mut Vec<ModelToolReturnPart>) {
262        match self {
263            Self::Null => push_text_part(parts, "null"),
264            Self::Bool(value) => push_text_part(parts, value.to_string()),
265            Self::Number(value) => push_text_part(parts, value.to_string()),
266            Self::String(value) => push_text_part(
267                parts,
268                serde_json::to_string(value).unwrap_or_else(|_| "\"\"".into()),
269            ),
270            Self::Attachment(reference) => {
271                parts.push(ModelToolReturnPart::Attachment(reference.clone()))
272            }
273            Self::Array(values) => {
274                push_text_part(parts, "[");
275                for (index, value) in values.iter().enumerate() {
276                    if index > 0 {
277                        push_text_part(parts, ",");
278                    }
279                    value.push_compact_model_parts(parts);
280                }
281                push_text_part(parts, "]");
282            }
283            Self::Object(entries) => {
284                push_text_part(parts, "{");
285                for (index, (key, value)) in entries.iter().enumerate() {
286                    if index > 0 {
287                        push_text_part(parts, ",");
288                    }
289                    push_text_part(
290                        parts,
291                        serde_json::to_string(key).unwrap_or_else(|_| "\"\"".into()),
292                    );
293                    push_text_part(parts, ":");
294                    value.push_compact_model_parts(parts);
295                }
296                push_text_part(parts, "}");
297            }
298        }
299    }
300}
301
302fn tagged_attachment_json(source: &AttachmentSource) -> Value {
303    let mut map = Map::with_capacity(2);
304    map.insert(
305        TAG_KEY.to_string(),
306        Value::String(ATTACHMENT_TAG.to_string()),
307    );
308    map.insert(
309        SOURCE_KEY.to_string(),
310        serde_json::to_value(source).unwrap_or(Value::Null),
311    );
312    Value::Object(map)
313}
314
315fn object_tool_value_to_json(entries: &BTreeMap<String, ToolValue>) -> Value {
316    let object = entries
317        .iter()
318        .map(|(key, value)| (key.clone(), value.to_json_value()))
319        .collect::<Map<_, _>>();
320    if entries.contains_key(TAG_KEY) {
321        escaped_object_tool_value_json(Value::Object(object))
322    } else {
323        Value::Object(object)
324    }
325}
326
327fn object_tool_value_into_json(entries: BTreeMap<String, ToolValue>) -> Value {
328    let contains_reserved_tag = entries.contains_key(TAG_KEY);
329    let object = entries
330        .into_iter()
331        .map(|(key, value)| (key, value.into_json_value()))
332        .collect::<Map<_, _>>();
333    if contains_reserved_tag {
334        escaped_object_tool_value_json(Value::Object(object))
335    } else {
336        Value::Object(object)
337    }
338}
339
340fn escaped_object_tool_value_json(entries: Value) -> Value {
341    let mut map = Map::with_capacity(2);
342    map.insert(TAG_KEY.to_string(), Value::String(OBJECT_TAG.to_string()));
343    map.insert(ENTRIES_KEY.to_string(), entries);
344    Value::Object(map)
345}
346
347impl From<Value> for ToolValue {
348    fn from(value: Value) -> Self {
349        match value {
350            Value::Null => Self::Null,
351            Value::Bool(value) => Self::Bool(value),
352            Value::Number(value) => Self::Number(value),
353            Value::String(value) => Self::String(value),
354            Value::Array(values) => Self::Array(values.into_iter().map(Self::from).collect()),
355            Value::Object(values) => Self::Object(
356                values
357                    .into_iter()
358                    .map(|(key, value)| (key, Self::from(value)))
359                    .collect(),
360            ),
361        }
362    }
363}
364
365impl From<&str> for ToolValue {
366    fn from(value: &str) -> Self {
367        Self::String(value.to_string())
368    }
369}
370
371impl From<String> for ToolValue {
372    fn from(value: String) -> Self {
373        Self::String(value)
374    }
375}
376
377impl Serialize for ToolValue {
378    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
379    where
380        S: Serializer,
381    {
382        match self {
383            Self::Null => serializer.serialize_none(),
384            Self::Bool(value) => serializer.serialize_bool(*value),
385            Self::Number(value) => value.serialize(serializer),
386            Self::String(value) => serializer.serialize_str(value),
387            Self::Array(values) => values.serialize(serializer),
388            Self::Attachment(source) => {
389                let mut map = serializer.serialize_map(Some(2))?;
390                map.serialize_entry(TAG_KEY, ATTACHMENT_TAG)?;
391                map.serialize_entry(SOURCE_KEY, source)?;
392                map.end()
393            }
394            Self::Object(entries) => {
395                if entries.contains_key(TAG_KEY) {
396                    let mut map = serializer.serialize_map(Some(2))?;
397                    map.serialize_entry(TAG_KEY, OBJECT_TAG)?;
398                    map.serialize_entry(ENTRIES_KEY, entries)?;
399                    map.end()
400                } else {
401                    entries.serialize(serializer)
402                }
403            }
404        }
405    }
406}
407
408impl<'de> Deserialize<'de> for ToolValue {
409    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
410    where
411        D: Deserializer<'de>,
412    {
413        struct ToolValueVisitor;
414
415        impl<'de> Visitor<'de> for ToolValueVisitor {
416            type Value = ToolValue;
417
418            fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
419                formatter.write_str("a Lash tool value")
420            }
421
422            fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> {
423                Ok(ToolValue::Bool(value))
424            }
425
426            fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> {
427                Ok(ToolValue::Number(Number::from(value)))
428            }
429
430            fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
431                Ok(ToolValue::Number(Number::from(value)))
432            }
433
434            fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
435            where
436                E: DeError,
437            {
438                Number::from_f64(value)
439                    .map(ToolValue::Number)
440                    .ok_or_else(|| E::custom("non-finite number is not a valid tool value"))
441            }
442
443            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> {
444                Ok(ToolValue::String(value.to_string()))
445            }
446
447            fn visit_string<E>(self, value: String) -> Result<Self::Value, E> {
448                Ok(ToolValue::String(value))
449            }
450
451            fn visit_none<E>(self) -> Result<Self::Value, E> {
452                Ok(ToolValue::Null)
453            }
454
455            fn visit_unit<E>(self) -> Result<Self::Value, E> {
456                Ok(ToolValue::Null)
457            }
458
459            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
460            where
461                A: serde::de::SeqAccess<'de>,
462            {
463                let mut values = Vec::new();
464                while let Some(value) = seq.next_element()? {
465                    values.push(value);
466                }
467                Ok(ToolValue::Array(values))
468            }
469
470            fn visit_map<A>(self, mut access: A) -> Result<Self::Value, A::Error>
471            where
472                A: MapAccess<'de>,
473            {
474                let mut map = Map::new();
475                while let Some((key, value)) = access.next_entry::<String, Value>()? {
476                    map.insert(key, value);
477                }
478                decode_object(map).map_err(A::Error::custom)
479            }
480        }
481
482        deserializer.deserialize_any(ToolValueVisitor)
483    }
484}
485
486fn decode_object(mut map: Map<String, Value>) -> serde_json::Result<ToolValue> {
487    let Some(tag) = map.get(TAG_KEY) else {
488        return Ok(ToolValue::Object(
489            map.into_iter()
490                .map(|(key, value)| Ok((key, ToolValue::from_json_value(value)?)))
491                .collect::<serde_json::Result<_>>()?,
492        ));
493    };
494    let tag = tag
495        .as_str()
496        .ok_or_else(|| serde_json::Error::custom("reserved tool value tag must be a string"))?;
497    match tag {
498        ATTACHMENT_TAG => {
499            if map.len() != 2 || !map.contains_key(SOURCE_KEY) {
500                return Err(serde_json::Error::custom("malformed attachment tool value"));
501            }
502            let source = serde_json::from_value(
503                map.remove(SOURCE_KEY)
504                    .ok_or_else(|| serde_json::Error::custom("missing attachment source"))?,
505            )?;
506            Ok(ToolValue::Attachment(source))
507        }
508        OBJECT_TAG => {
509            if map.len() != 2 || !map.contains_key(ENTRIES_KEY) {
510                return Err(serde_json::Error::custom(
511                    "malformed escaped object tool value",
512                ));
513            }
514            serde_json::from_value(
515                map.remove(ENTRIES_KEY)
516                    .ok_or_else(|| serde_json::Error::custom("missing escaped object entries"))?,
517            )
518            .map(ToolValue::Object)
519        }
520        other => Err(serde_json::Error::custom(format!(
521            "unknown reserved tool value tag `{other}`"
522        ))),
523    }
524}
525
526#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
527pub struct ToolFailure {
528    pub class: ToolFailureClass,
529    pub code: String,
530    pub message: String,
531    pub source: ToolFailureSource,
532    pub retry: ToolRetryDisposition,
533    #[serde(default, skip_serializing_if = "Option::is_none")]
534    pub raw: Option<ToolValue>,
535}
536
537impl ToolFailure {
538    pub fn new(
539        class: ToolFailureClass,
540        code: impl Into<String>,
541        message: impl Into<String>,
542    ) -> Self {
543        Self {
544            class,
545            code: code.into(),
546            message: message.into(),
547            source: ToolFailureSource::Runtime,
548            retry: ToolRetryDisposition::Never,
549            raw: None,
550        }
551    }
552
553    pub fn runtime(
554        class: ToolFailureClass,
555        code: impl Into<String>,
556        message: impl Into<String>,
557    ) -> Self {
558        Self::new(class, code, message)
559    }
560
561    pub fn tool(
562        class: ToolFailureClass,
563        code: impl Into<String>,
564        message: impl Into<String>,
565    ) -> Self {
566        Self {
567            source: ToolFailureSource::Tool,
568            ..Self::new(class, code, message)
569        }
570    }
571
572    pub fn safe_retry(
573        class: ToolFailureClass,
574        code: impl Into<String>,
575        message: impl Into<String>,
576        after_ms: Option<u64>,
577    ) -> Self {
578        let mut failure = Self::tool(class, code, message);
579        failure.retry = ToolRetryDisposition::Safe { after_ms };
580        failure
581    }
582
583    pub fn with_retry(mut self, retry: ToolRetryDisposition) -> Self {
584        self.retry = retry;
585        self
586    }
587
588    pub fn to_json_value(&self) -> Value {
589        serde_json::to_value(self).unwrap_or_else(|_| Value::String(self.message.clone()))
590    }
591}
592
593#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
594#[serde(rename_all = "snake_case")]
595pub enum ToolFailureClass {
596    InvalidRequest,
597    Unavailable,
598    PermissionDenied,
599    Timeout,
600    Execution,
601    External,
602    ResourceLimit,
603    Internal,
604}
605
606#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
607#[serde(rename_all = "snake_case")]
608pub enum ToolFailureSource {
609    Runtime,
610    Tool,
611    Plugin,
612    Policy,
613    Cancellation,
614}
615
616#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
617#[serde(tag = "type", rename_all = "snake_case")]
618pub enum ToolRetryDisposition {
619    Never,
620    Safe {
621        #[serde(default, skip_serializing_if = "Option::is_none")]
622        after_ms: Option<u64>,
623    },
624    Exhausted {
625        attempts: u32,
626    },
627}
628
629#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
630pub struct ToolCancellation {
631    pub message: String,
632    pub source: ToolFailureSource,
633    #[serde(default, skip_serializing_if = "Option::is_none")]
634    pub raw: Option<ToolValue>,
635}
636
637impl ToolCancellation {
638    pub fn runtime(message: impl Into<String>) -> Self {
639        Self {
640            message: message.into(),
641            source: ToolFailureSource::Cancellation,
642            raw: None,
643        }
644    }
645
646    pub fn to_json_value(&self) -> Value {
647        serde_json::to_value(self).unwrap_or_else(|_| Value::String(self.message.clone()))
648    }
649}
650
651#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
652#[serde(tag = "type", rename_all = "snake_case")]
653pub enum ToolControl {
654    SwitchAgentFrame {
655        frame_id: String,
656        #[serde(default, skip_serializing_if = "Vec::is_empty")]
657        initial_nodes: Vec<crate::SessionAppendNode>,
658        #[serde(default, skip_serializing_if = "Option::is_none")]
659        task: Option<String>,
660    },
661    Finish {
662        value: ToolValue,
663    },
664    Fail {
665        failure: ToolFailure,
666    },
667}
668
669#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
670pub struct ModelToolReturn {
671    pub call_id: String,
672    pub tool_name: String,
673    pub parts: Vec<ModelToolReturnPart>,
674}
675
676impl ModelToolReturn {
677    pub fn from_output(call_id: String, tool_name: String, output: &ToolCallOutput) -> Self {
678        let parts = model_parts_from_tool_output(output);
679        Self {
680            call_id,
681            tool_name,
682            parts,
683        }
684    }
685
686    pub fn text(call_id: String, tool_name: String, content: impl Into<String>) -> Self {
687        Self {
688            call_id,
689            tool_name,
690            parts: vec![ModelToolReturnPart::text(content)],
691        }
692    }
693}
694
695#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
696#[serde(tag = "type", rename_all = "snake_case")]
697pub enum ModelToolReturnPart {
698    Text { text: String },
699    Attachment(AttachmentSource),
700}
701
702impl ModelToolReturnPart {
703    pub fn text(text: impl Into<String>) -> Self {
704        Self::Text { text: text.into() }
705    }
706}
707
708pub fn model_parts_from_tool_output(output: &ToolCallOutput) -> Vec<ModelToolReturnPart> {
709    match &output.outcome {
710        ToolCallOutcome::Success(value) => value.model_parts(),
711        ToolCallOutcome::Failure(failure) => {
712            let mut parts = vec![ModelToolReturnPart::text(format_failure_message(failure))];
713            if let Some(raw) = &failure.raw {
714                parts.extend(
715                    raw.attachments()
716                        .into_iter()
717                        .map(ModelToolReturnPart::Attachment),
718                );
719            }
720            parts
721        }
722        ToolCallOutcome::Cancelled(cancellation) => {
723            let mut parts = vec![ModelToolReturnPart::text(format_cancellation_message(
724                cancellation,
725            ))];
726            if let Some(raw) = &cancellation.raw {
727                parts.extend(
728                    raw.attachments()
729                        .into_iter()
730                        .map(ModelToolReturnPart::Attachment),
731                );
732            }
733            parts
734        }
735    }
736}
737
738fn push_text_part(parts: &mut Vec<ModelToolReturnPart>, text: impl Into<String>) {
739    let text = text.into();
740    if text.is_empty() {
741        return;
742    }
743    if let Some(ModelToolReturnPart::Text { text: existing }) = parts.last_mut() {
744        existing.push_str(&text);
745    } else {
746        parts.push(ModelToolReturnPart::text(text));
747    }
748}
749
750fn format_failure_message(failure: &ToolFailure) -> String {
751    if failure.message.is_empty() {
752        "[Tool execution failed]".to_string()
753    } else {
754        format!("[Tool execution failed]\n{}", failure.message)
755    }
756}
757
758fn format_cancellation_message(cancellation: &ToolCancellation) -> String {
759    if cancellation.message.is_empty() {
760        "[Tool execution cancelled]".to_string()
761    } else {
762        format!("[Tool execution cancelled]\n{}", cancellation.message)
763    }
764}
765
766#[cfg(test)]
767mod tests {
768    use super::*;
769    use crate::{AttachmentId, AttachmentMeta, AttachmentTypeMetadata, MediaType};
770
771    fn attachment_source(id: &str) -> AttachmentSource {
772        AttachmentSource::stored(
773            AttachmentMeta::new(
774                AttachmentId::new(id),
775                MediaType::parse("image/png").unwrap(),
776                3,
777                Some(AttachmentTypeMetadata::image(Some(1), Some(1))),
778                Some("tiny".to_string()),
779            )
780            .as_ref(),
781        )
782    }
783
784    #[test]
785    fn tool_value_serializes_nested_attachments() {
786        let value = ToolValue::Array(vec![ToolValue::Attachment(attachment_source("img"))]);
787
788        let json = serde_json::to_value(&value).unwrap();
789
790        assert_eq!(json[0][TAG_KEY], ATTACHMENT_TAG);
791        assert_eq!(json[0][SOURCE_KEY]["attachment_ref"]["id"], "img");
792        assert_eq!(serde_json::from_value::<ToolValue>(json).unwrap(), value);
793    }
794
795    #[test]
796    fn tool_value_escapes_user_reserved_key() {
797        let value = ToolValue::Object(BTreeMap::from([(
798            TAG_KEY.to_string(),
799            ToolValue::String("user".into()),
800        )]));
801
802        let json = serde_json::to_value(&value).unwrap();
803
804        assert_eq!(json[TAG_KEY], OBJECT_TAG);
805        assert!(json[ENTRIES_KEY].is_object());
806        assert_eq!(serde_json::from_value::<ToolValue>(json).unwrap(), value);
807    }
808
809    #[test]
810    fn consuming_projection_matches_tool_value_serialization() {
811        let value = ToolValue::Object(BTreeMap::from([
812            (
813                "attachment".to_string(),
814                ToolValue::Attachment(attachment_source("img")),
815            ),
816            (
817                TAG_KEY.to_string(),
818                ToolValue::Array(vec![ToolValue::String("user".into())]),
819            ),
820        ]));
821        let serialized = serde_json::to_value(&value).unwrap();
822        assert_eq!(value.to_json_value(), serialized);
823
824        let output = ToolCallOutput::success(value);
825        assert_eq!(output.into_value_for_projection(), serialized);
826    }
827
828    #[test]
829    fn tool_value_rejects_malformed_reserved_object() {
830        let json = serde_json::json!({ TAG_KEY: ATTACHMENT_TAG, "extra": true });
831
832        assert!(serde_json::from_value::<ToolValue>(json).is_err());
833    }
834
835    #[test]
836    fn tool_value_model_parts_preserve_attachment_position() {
837        let value = ToolValue::Array(vec![
838            ToolValue::String("before".into()),
839            ToolValue::Attachment(attachment_source("img")),
840            ToolValue::String("after".into()),
841        ]);
842
843        assert_eq!(
844            value.model_parts(),
845            vec![
846                ModelToolReturnPart::text("[\"before\","),
847                ModelToolReturnPart::Attachment(attachment_source("img")),
848                ModelToolReturnPart::text(",\"after\"]"),
849            ]
850        );
851    }
852
853    #[test]
854    fn tool_output_failure_projects_raw_attachments_after_failure_text() {
855        let attachment = attachment_source("img");
856        let output = ToolCallOutput::failure(ToolFailure {
857            class: ToolFailureClass::Execution,
858            code: "boom".into(),
859            message: "boom".into(),
860            source: ToolFailureSource::Tool,
861            retry: ToolRetryDisposition::Never,
862            raw: Some(ToolValue::Object(BTreeMap::from([(
863                "image".into(),
864                ToolValue::Attachment(attachment.clone()),
865            )]))),
866        });
867
868        assert_eq!(
869            model_parts_from_tool_output(&output),
870            vec![
871                ModelToolReturnPart::text("[Tool execution failed]\nboom"),
872                ModelToolReturnPart::Attachment(attachment),
873            ]
874        );
875    }
876
877    #[test]
878    fn model_tool_return_text_part_serializes() {
879        let part = ModelToolReturnPart::text("hello");
880
881        let json = serde_json::to_value(&part).unwrap();
882
883        assert_eq!(json, serde_json::json!({ "type": "text", "text": "hello" }));
884        assert_eq!(
885            serde_json::from_value::<ModelToolReturnPart>(json).unwrap(),
886            part
887        );
888    }
889
890    #[test]
891    fn tool_output_status_distinguishes_cancelled_from_failure() {
892        let failure = ToolCallOutput::failure(ToolFailure::tool(
893            ToolFailureClass::Execution,
894            "boom",
895            "boom",
896        ));
897        let cancelled = ToolCallOutput::cancelled(ToolCancellation::runtime("stopped"));
898
899        assert_eq!(failure.status(), ToolCallStatus::Failure);
900        assert_eq!(cancelled.status(), ToolCallStatus::Cancelled);
901        assert!(!cancelled.is_success());
902    }
903}