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