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