conversation_api/execution/
tool_result.rs1use super::ResponseDetail;
3use serde_json::{Value, json};
4
5pub fn essential_yaml(value: &Value) -> Result<String, serde_yaml::Error> {
9 fn omit_null_fields(value: &mut Value) {
10 match value {
11 Value::Object(fields) => {
12 fields.retain(|_, value| !value.is_null());
13 fields.values_mut().for_each(omit_null_fields);
14 }
15 Value::Array(items) => items.iter_mut().for_each(omit_null_fields),
16 _ => {}
17 }
18 }
19 let mut projected = value.clone();
20 omit_null_fields(&mut projected);
21 serde_yaml::to_string(&projected)
22}
23
24pub fn tool_result_text(value: &Value, detail: ResponseDetail) -> String {
27 if let Some(text) = (detail == ResponseDetail::Essential)
28 .then(|| value.get("data").and_then(Value::as_str))
29 .flatten()
30 && matches!(
31 value.get("status").and_then(Value::as_str),
32 Some("success" | "partial" | "error")
33 )
34 {
35 let warnings = value
36 .get("warnings")
37 .filter(|v| v.as_array().is_some_and(|v| !v.is_empty()));
38 let extra_error_details =
39 value["status"] == "error" && value["error"].get("details").is_some();
40 if value["status"] != "partial" && warnings.is_none() && !extra_error_details {
41 return text.to_owned();
42 }
43 let result =
44 serde_yaml::from_str::<Value>(text).unwrap_or_else(|_| Value::String(text.into()));
45 let mut visible = json!({"status":value["status"],"result":result});
46 if extra_error_details {
47 visible["error"] = value["error"].clone();
48 }
49 if let Some(warnings) = warnings {
50 visible["warnings"] = warnings.clone();
51 }
52 return essential_yaml(&visible).expect("serializable result");
53 }
54 value
55 .as_str()
56 .map(str::to_owned)
57 .unwrap_or_else(|| value.to_string())
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63 #[test]
64 fn essential_omits_null_fields_without_losing_values_or_array_positions() {
65 let full = json!({"missing":null,"nested":{"missing":null,"enabled":false,
66 "zero":0,"empty":"","list":[],"object":{},"literal":"null",
67 "profile":"missing: null\nKeep this text\n"},"rows":[null,{"missing":null,"id":"a"},{}]});
68 let before = full.clone();
69 let parsed: Value = serde_yaml::from_str(&essential_yaml(&full).unwrap()).unwrap();
70 assert_eq!(
71 parsed,
72 json!({"nested":{"enabled":false,"zero":0,"empty":"",
73 "list":[],"object":{},"literal":"null","profile":"missing: null\nKeep this text\n"},
74 "rows":[null,{"id":"a"},{}]})
75 );
76 assert_eq!(full, before);
77 assert_eq!(essential_yaml(&Value::Null).unwrap(), "null\n");
78 }
79
80 #[test]
81 fn late_warning_and_error_details_follow_the_same_null_rule() {
82 let value = json!({"status":"error","error":{"code":"FAILED","message":"Failed",
83 "details":{"missing":null,"clientReport":{"verified":false,"missing":null}}},
84 "data":"status: error\n", "warnings":[{"code":"LATE","message":"Late warning",
85 "details":{"missing":null,"retry":false}}]});
86 let parsed: Value =
87 serde_yaml::from_str(&tool_result_text(&value, ResponseDetail::Essential)).unwrap();
88 assert_eq!(
89 parsed["error"]["details"],
90 json!({"clientReport":{"verified":false}})
91 );
92 assert_eq!(parsed["warnings"][0]["details"], json!({"retry":false}));
93 assert!(value["error"]["details"].get("missing").is_some());
94 }
95 #[test]
96 fn visible_text_preserves_partial_outcomes_and_warnings() {
97 let full_string = json!({"status":"success","data":"items: []\n"});
98 assert_eq!(
99 tool_result_text(&full_string, ResponseDetail::Full),
100 full_string.to_string()
101 );
102 assert_eq!(
103 tool_result_text(&json!({"status":"success"}), ResponseDetail::Essential),
104 r#"{"status":"success"}"#
105 );
106 for status in ["success", "partial"] {
107 let value = json!({"status":status,"data":"items: []\n", "warnings":[{"code":"UNAVAILABLE","message":"One source is offline"}]});
108 let text = tool_result_text(&value, ResponseDetail::Essential);
109 let yaml: Value = serde_yaml::from_str(&text).unwrap();
110 assert_eq!(yaml["status"], status);
111 assert_eq!(yaml["warnings"], value["warnings"]);
112 assert_eq!(yaml["result"], json!({"items":[]}));
113 }
114 assert_eq!(
115 tool_result_text(
116 &json!({"status":"success","data":"items: []\n"}),
117 ResponseDetail::Essential
118 ),
119 "items: []\n"
120 );
121 assert_eq!(
122 tool_result_text(
123 &json!({"data":{"status":"error"}}),
124 ResponseDetail::Essential
125 ),
126 r#"{"data":{"status":"error"}}"#
127 );
128 }
129 #[test]
130 fn late_error_observations_survive_visible_output() {
131 let value = json!({"status":"error", "error":{"code":"FAILED","message":"Failed", "details":{"clientReport":{"verified":false}}}, "data":"status: error\nerror:\n code: FAILED\n message: Failed\n"});
132 let visible: Value =
133 serde_yaml::from_str(&tool_result_text(&value, ResponseDetail::Essential)).unwrap();
134 assert_eq!(visible["error"]["details"], value["error"]["details"]);
135 assert_eq!(visible["result"]["status"], "error");
136 }
137}