Skip to main content

conversation_api/execution/
tool_result.rs

1//! Provider-visible text for canonical tool results; presentation never changes history.
2use super::ResponseDetail;
3use serde_json::{Value, json};
4
5/// Serialize an Essential projection, omitting null-valued object fields recursively.
6/// Keep array positions, root null, empty collections, booleans, numbers and text intact.
7/// This presentation rule never mutates the full business value or parses string content.
8pub 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
24/// Text at model/MCP/CLI boundaries. Canonical history keeps the original outcome.
25/// Full responses and non-Facade values retain JSON; Essential content is never JSON-escaped.
26pub 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}