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}