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}