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}