Skip to main content

protocol_droid/application/server/
result.rs

1//! The JSON a generated `#[tool]` handler answers with.
2//!
3//! # Why this lives here and not in the macro
4//!
5//! A generated handler has to turn two Rust things into one wire thing: a
6//! deserialised argument struct, and a `Result<T, E>`. The shape of a failed
7//! tool call — a text block under `content` with `isError` set — is the
8//! specification's, not the macro author's. A macro that built the JSON itself
9//! would be free to disagree with [`ToolResult::error`](crate::domain::ir::ToolResult::error)
10//! and with the codec that lowers it, and nothing would notice until a client
11//! read the difference.
12//!
13//! So the macro generates a call into [`success`] and [`failure`] and owns no
14//! document shape at all.
15//!
16//! # Why `structuredContent` is decided here rather than by a codec
17//!
18//! It is not decided here — this function emits it, and the codec drops it for
19//! a revision before `2025-06-18`. That split is deliberate: whether the field
20//! _exists_ is a codec question, and whether the answer _has_ an object in it
21//! is a Rust question, and putting both in one place would make the codec
22//! re-derive a type it should be handed.
23
24use serde::Serialize;
25
26use crate::domain::ir::{JsonObject, JsonObjectError};
27
28/// Why a generated handler could not produce an answer.
29#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
30pub enum OutcomeError {
31    /// The peer's `arguments` object did not deserialise into the handler's
32    /// argument struct.
33    ///
34    /// Carries the deserialiser's own message. A second, shorter vocabulary for
35    /// "that is not the right shape" would lose the one field a caller can act
36    /// on, which is *which* field and *why*.
37    #[error("the arguments do not match this tool: {0}")]
38    Arguments(String),
39
40    /// The handler's return value could not be serialised.
41    ///
42    /// A `Serialize` impl that fails is a bug in the handler's types — a
43    /// non-string map key, a NaN — not a bad request, and the message says so
44    /// rather than blaming the peer.
45    #[error("the handler's answer cannot be encoded: {0}")]
46    Answer(String),
47
48    /// The assembled document was not a JSON object.
49    ///
50    /// Cannot happen for a value this function builds, and propagated rather
51    /// than swallowed so that a future member with a different shape fails
52    /// loudly instead of producing an empty result.
53    #[error("the assembled result is not a JSON object: {0}")]
54    NotAnObject(JsonObjectError),
55
56    /// A handler answered with a [`ToolResult`](crate::domain::ir::ToolResult)
57    /// the negotiated revision cannot express.
58    ///
59    /// Reached only from `codec::result::verbatim`, which is the path a
60    /// `#[tool]` takes when its return type is a `ToolResult`. Carries the
61    /// codec's own error rather than a paraphrase of it: the revision, the
62    /// feature, and whether a policy named a rule for it are three different
63    /// fixes and the caller cannot pick between them without all three.
64    #[error("the result cannot be expressed at this revision: {0}")]
65    NotLowerable(crate::domain::ir::LowerError),
66}
67
68/// A handler's answer, as a `tools/call` result.
69///
70/// An object answer becomes `structuredContent` **and** a text block holding
71/// the same JSON. The duplication is the specification's own recommendation,
72/// not redundancy: a `2025-06-18` client reads `structuredContent` and a
73/// `2024-11-05` one reads the text, and a result carrying only the former is
74/// unreadable to the latter. A non-object answer is a text block alone, since
75/// there is no structured form to carry it.
76///
77/// `structuredContent` here is a `2025-06-18`-or-later value. A codec targeting
78/// an earlier revision drops it, and that is the codec's call to make.
79///
80/// # Errors
81///
82/// [`OutcomeError::Answer`] when `answer` will not serialise — a bug in the
83/// handler's own types.
84pub fn success(answer: &impl Serialize) -> Result<JsonObject, OutcomeError> {
85    let value =
86        serde_json::to_value(answer).map_err(|error| OutcomeError::Answer(error.to_string()))?;
87
88    let (structured, text) = match &value {
89        serde_json::Value::Object(_) => (Some(value.clone()), value.to_string()),
90        // A string answer is its own text. Taking `to_string` of it would give
91        // `"hello"` *including the quotes*, because that is the JSON encoding
92        // of a string rather than the string — a detail visible to every user
93        // of a tool that returns a message.
94        serde_json::Value::String(text) => (None, text.clone()),
95        // `null` and everything else are their JSON form. There is no shorter
96        // honest rendering of `42` that is still the same value.
97        other => (None, other.to_string()),
98    };
99
100    let mut document = serde_json::json!({
101        "content": [{ "type": "text", "text": text }],
102        "isError": false,
103    });
104    if let (serde_json::Value::Object(map), Some(structured)) = (&mut document, structured) {
105        map.insert("structuredContent".to_string(), structured);
106    }
107
108    JsonObject::from_value(document).map_err(OutcomeError::NotAnObject)
109}
110
111/// A handler's failure, as a `tools/call` result.
112///
113/// Always an error result rather than a transport failure, because a tool that
114/// refuses is a normal answer: the call reached the tool and the tool said no.
115/// The distinction matters to a caller deciding whether to retry, and folding
116/// the two together would make every refusal look like a broken connection.
117#[must_use]
118pub fn failure(message: impl core::fmt::Display) -> JsonObject {
119    let document = serde_json::json!({
120        "content": [{ "type": "text", "text": message.to_string() }],
121        "isError": true,
122    });
123    JsonObject::from_value(document).unwrap_or_default()
124}
125
126#[cfg(test)]
127mod tests {
128    use serde::Serialize;
129
130    use super::{OutcomeError, failure, success};
131    use crate::domain::ir::JsonObject;
132
133    /// A return type that serialises to a JSON object.
134    #[derive(Serialize)]
135    struct ObjectAnswer {
136        /// A member, so the object is not empty.
137        total: i32,
138    }
139
140    /// A return type that serialises to a bare string.
141    #[derive(Serialize)]
142    struct TextAnswer(String);
143
144    /// The document's `content` member, as the text blocks it holds.
145    fn text_blocks(document: &JsonObject) -> Result<Vec<String>, String> {
146        let parsed: serde_json::Value =
147            serde_json::from_str(document.as_str()).map_err(|error| error.to_string())?;
148        parsed
149            .get("content")
150            .and_then(serde_json::Value::as_array)
151            .ok_or("the result has no content array")?
152            .iter()
153            .map(|block| {
154                block
155                    .get("text")
156                    .and_then(serde_json::Value::as_str)
157                    .map(str::to_string)
158                    .ok_or_else(|| "a content block is not text".to_string())
159            })
160            .collect()
161    }
162
163    /// An object answer carries `structuredContent` and a text copy of it.
164    ///
165    /// The duplication is what lets a pre-`2025-06-18` client read the result
166    /// at all, so it is asserted rather than left to a reader to infer.
167    #[test]
168    fn an_object_answer_becomes_structured_content_and_a_text_copy() -> Result<(), String> {
169        let document = success(&ObjectAnswer { total: 7 }).map_err(|e| e.to_string())?;
170        let parsed: serde_json::Value =
171            serde_json::from_str(document.as_str()).map_err(|error| error.to_string())?;
172
173        assert_eq!(
174            parsed
175                .get("structuredContent")
176                .and_then(|value| value.get("total")),
177            Some(&serde_json::Value::from(7)),
178            "the object answer did not reach structuredContent"
179        );
180        let blocks = text_blocks(&document)?;
181        if blocks.first() != Some(&"{\"total\":7}".to_string()) {
182            return Err(format!("the text copy is not the object: {blocks:?}"));
183        }
184        Ok(())
185    }
186
187    /// A non-object answer is a text block alone, with no structured member.
188    #[test]
189    fn a_text_answer_becomes_a_text_block_and_nothing_else() -> Result<(), String> {
190        let document = success(&TextAnswer("hello".to_string())).map_err(|e| e.to_string())?;
191        let parsed: serde_json::Value =
192            serde_json::from_str(document.as_str()).map_err(|error| error.to_string())?;
193
194        if parsed.get("structuredContent").is_some() {
195            return Err("a text answer produced structuredContent".to_string());
196        }
197        if text_blocks(&document)? != ["hello".to_string()] {
198            return Err("the text answer did not survive".to_string());
199        }
200        Ok(())
201    }
202
203    /// A success is never an error result, whatever it carries.
204    #[test]
205    fn a_success_is_never_flagged_as_an_error() -> Result<(), String> {
206        let document = success(&ObjectAnswer { total: 0 }).map_err(|e| e.to_string())?;
207        let parsed: serde_json::Value =
208            serde_json::from_str(document.as_str()).map_err(|error| error.to_string())?;
209        assert_eq!(parsed.get("isError"), Some(&serde_json::Value::Bool(false)));
210        Ok(())
211    }
212
213    /// A failure carries the handler's message and is flagged.
214    #[test]
215    fn a_failure_carries_its_message_and_is_flagged() -> Result<(), String> {
216        let document = failure("the widget is missing");
217        let parsed: serde_json::Value =
218            serde_json::from_str(document.as_str()).map_err(|error| error.to_string())?;
219        assert_eq!(parsed.get("isError"), Some(&serde_json::Value::Bool(true)));
220        if text_blocks(&document)? != ["the widget is missing".to_string()] {
221            return Err("the failure lost its message".to_string());
222        }
223        Ok(())
224    }
225
226    /// A `Serialize` that cannot produce a value is the handler's bug, and the
227    /// error says so rather than blaming the peer.
228    #[test]
229    fn an_unserialisable_answer_is_reported_as_the_handlers_fault() -> Result<(), String> {
230        match success(&NotSerializable) {
231            Err(OutcomeError::Answer(_)) => Ok(()),
232            Err(other) => Err(format!("wrong error for a bad answer: {other}")),
233            Ok(document) => Err(format!("a bad answer produced {document}")),
234        }
235    }
236
237    /// A type whose `Serialize` fails, which no handler should return.
238    struct NotSerializable;
239
240    impl Serialize for NotSerializable {
241        fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
242        where
243            S: serde::Serializer,
244        {
245            Err(serde::ser::Error::custom("this value has no JSON form"))
246        }
247    }
248}