protocol_droid/domain/a2a/send_message.rs
1//! A2A `SendMessage` typed envelopes — the
2//! request, the configuration, and the response.
3//!
4//! # What these are
5//!
6//! [`SendMessageRequest`] is the typed shape of
7//! the `params` field for the `SendMessage`
8//! JSON-RPC method. [`SendMessageResponse`] is
9//! the typed shape of the `result` field —
10//! either a [`Task`] (the work is in progress or
11//! complete) or a [`Message`] (the agent
12//! answered directly without creating a task).
13//!
14//! The handler that implements
15//! [`A2aRequestHandler`](crate::application::a2a::A2aRequestHandler)
16//! decodes the raw `serde_json::Value` params
17//! into a `SendMessageRequest` and serialises
18//! the result back as a `SendMessageResponse`.
19//! The serving adapter
20//! ([`serve_jsonrpc`](crate::adapters::primary::a2a::jsonrpc_serve::serve_jsonrpc))
21//! handles the JSON-RPC envelope; the handler
22//! is responsible only for the method-specific
23//! decoding.
24//!
25//! # Why an untagged enum for the response
26//!
27//! The wire form of a `SendMessage` response is
28//! the inner object directly: a `Task` is
29//! `{"id": "...", "status": ...}`, a `Message`
30//! is `{"role": ..., "parts": ...}`. The two
31//! shapes are discriminated by which top-level
32//! field is present (`id` for `Task`, `role` for
33//! `Message`). An untagged enum matches the
34//! wire form; a tagged enum
35//! (`{"kind": "task", "task": {...}}`) would
36//! add a key the spec does not name.
37//!
38//! # Why `accepted_output_modes` is `BTreeSet`
39//! and not `Vec`
40//!
41//! The `accepted_output_modes` list is a
42//! set of MIME types the client can accept;
43//! a duplicate is a wire bug. The Rust
44//! type that surfaces a duplicate is a
45//! `BTreeSet` (ordered, so the wire form is
46//! deterministic; set, so duplicates fail
47//! at the type level). The trade-off is
48//! losing the order the client sent —
49//! which the spec does not require
50//! preserving.
51
52use std::collections::{BTreeMap, BTreeSet};
53
54use serde::{Deserialize, Serialize};
55
56use super::message::Message;
57use super::task::Task;
58
59/// A typed `SendMessage` request. Wire form:
60/// ```json
61/// {
62/// "message": { "role": "user", "parts": [...] },
63/// "configuration"?: { ... },
64/// "metadata"?: { ... }
65/// }
66/// ```
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(rename_all = "camelCase")]
69pub struct SendMessageRequest {
70 /// The message to send. Required.
71 pub message: Message,
72 /// Optional configuration for the request.
73 /// An absent `configuration` means the
74 /// agent uses its defaults.
75 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub configuration: Option<MessageSendConfiguration>,
77 /// Per-request metadata. The spec leaves
78 /// the shape open; the conventional keys
79 /// are `"trace_id"`, `"session_id"`, and
80 /// the custom names a particular
81 /// deployment uses.
82 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
83 pub metadata: BTreeMap<String, serde_json::Value>,
84}
85
86impl SendMessageRequest {
87 /// Starts a new request with `message`. Use
88 /// [`with_configuration`](Self::with_configuration)
89 /// to set the optional configuration.
90 #[must_use]
91 pub const fn new(message: Message) -> Self {
92 Self {
93 message,
94 configuration: None,
95 metadata: BTreeMap::new(),
96 }
97 }
98
99 /// Sets the configuration.
100 #[must_use]
101 pub fn with_configuration(mut self, configuration: MessageSendConfiguration) -> Self {
102 self.configuration = Some(configuration);
103 self
104 }
105}
106
107/// Configuration for a `SendMessage` request.
108///
109/// Wire form:
110/// ```json
111/// {
112/// "acceptedOutputModes": ["text/plain", "application/json"],
113/// "historyLength": 10,
114/// "blocking": true
115/// }
116/// ```
117///
118/// All fields are optional. A configuration
119/// with every field set to its default (an
120/// empty `accepted_output_modes`, no history
121/// length, no blocking) serialises to `{}` on
122/// the wire.
123#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct MessageSendConfiguration {
126 /// The MIME types the client can accept.
127 /// A client that sends a message but the
128 /// agent's reply is in a type not in this
129 /// list is a wire contract violation; the
130 /// agent is free to refuse with a typed
131 /// error.
132 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
133 pub accepted_output_modes: BTreeSet<String>,
134 /// The number of history messages to
135 /// include in the response. A request with
136 /// no history length lets the agent choose.
137 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub history_length: Option<u32>,
139 /// Whether the request should block
140 /// until the task completes. A
141 /// non-blocking request returns a `Task`
142 /// in `Submitted` or `Working` state;
143 /// the client polls with `GetTask` to
144 /// advance.
145 #[serde(default, skip_serializing_if = "is_false")]
146 pub blocking: bool,
147}
148
149// `skip_serializing_if` requires `fn(&T) -> bool`,
150// so the argument is `&bool` even though the
151// type is one byte. The clippy lint is a false
152// positive for serde-internal helpers.
153#[expect(
154 clippy::trivially_copy_pass_by_ref,
155 reason = "required by skip_serializing_if"
156)]
157const fn is_false(b: &bool) -> bool {
158 !*b
159}
160
161impl MessageSendConfiguration {
162 /// Starts a new configuration with the
163 /// accepted output modes from `modes`. Use
164 /// [`with_history_length`](Self::with_history_length)
165 /// and [`with_blocking`](Self::with_blocking)
166 /// to set the other fields.
167 #[must_use]
168 pub fn new(modes: impl IntoIterator<Item = impl Into<String>>) -> Self {
169 Self {
170 accepted_output_modes: modes.into_iter().map(Into::into).collect(),
171 history_length: None,
172 blocking: false,
173 }
174 }
175
176 /// Sets the history length.
177 #[must_use]
178 pub const fn with_history_length(mut self, length: u32) -> Self {
179 self.history_length = Some(length);
180 self
181 }
182
183 /// Marks the request as blocking.
184 #[must_use]
185 pub const fn with_blocking(mut self) -> Self {
186 self.blocking = true;
187 self
188 }
189}
190
191/// A typed `SendMessage` response.
192///
193/// The wire form is the inner object directly —
194/// a `Task` is `{"id": "...", "status": ...}`,
195/// a `Message` is `{"role": ..., "parts": ...}`.
196/// The two shapes are discriminated by which
197/// top-level field is present (`id` for `Task`,
198/// `role` for `Message`).
199#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
200#[serde(untagged)]
201pub enum SendMessageResponse {
202 /// The agent created (or is creating) a
203 /// task to do the work. The task's
204 /// `status.state` is one of `Submitted`,
205 /// `Working`, `Completed`, `Failed`,
206 /// `Cancelled`, or `InputRequired`. For
207 /// a blocking request, the state is
208 /// `Completed`, `Failed`, `Cancelled`, or
209 /// `InputRequired` by the time the
210 /// response is sent.
211 Task(Task),
212 /// The agent answered directly without
213 /// creating a task. The message is the
214 /// agent's reply.
215 Message(Message),
216}
217
218impl From<Task> for SendMessageResponse {
219 fn from(task: Task) -> Self {
220 Self::Task(task)
221 }
222}
223
224impl From<Message> for SendMessageResponse {
225 fn from(message: Message) -> Self {
226 Self::Message(message)
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::{MessageSendConfiguration, SendMessageRequest, SendMessageResponse};
233 use crate::domain::a2a::message::{Message, MessageRole, Part, TextPart};
234 use crate::domain::a2a::task::{Artifact, Task, TaskState, TaskStatus};
235
236 type TestError = Box<dyn std::error::Error>;
237
238 fn user_message() -> Message {
239 Message::new(MessageRole::User).with_part(Part::Text(TextPart {
240 text: "Hello.".to_owned(),
241 metadata: std::collections::BTreeMap::new(),
242 }))
243 }
244
245 // --- SendMessageRequest ------------------------------------------
246
247 #[test]
248 fn request_serialises_with_message_field() -> Result<(), TestError> {
249 let request = SendMessageRequest::new(user_message());
250 let value = serde_json::to_value(&request)?;
251 let obj = value
252 .as_object()
253 .ok_or("a SendMessageRequest must serialise to a JSON object")?;
254 assert!(obj.contains_key("message"));
255 // The optional fields are absent on
256 // the wire when they are `None` /
257 // empty.
258 assert!(!obj.contains_key("configuration"));
259 assert!(!obj.contains_key("metadata"));
260 Ok(())
261 }
262
263 #[test]
264 fn request_round_trips_with_configuration() -> Result<(), TestError> {
265 let request = SendMessageRequest::new(user_message()).with_configuration(
266 MessageSendConfiguration::new(["text/plain", "application/json"])
267 .with_history_length(10)
268 .with_blocking(),
269 );
270 let json = serde_json::to_string(&request)?;
271 let parsed: SendMessageRequest = serde_json::from_str(&json)?;
272 assert_eq!(parsed, request);
273 Ok(())
274 }
275
276 // --- MessageSendConfiguration ------------------------------------
277
278 #[test]
279 fn configuration_with_no_fields_is_minimal() -> Result<(), TestError> {
280 // A configuration with every field
281 // at its default serialises to `{}`
282 // on the wire. The `blocking` field
283 // is `false` by default, the
284 // `accepted_output_modes` is empty,
285 // and `history_length` is `None`.
286 let config = MessageSendConfiguration::default();
287 let value = serde_json::to_value(&config)?;
288 let obj = value
289 .as_object()
290 .ok_or("a MessageSendConfiguration must serialise to a JSON object")?;
291 assert!(obj.is_empty(), "expected `{{}}`, got {value}");
292 Ok(())
293 }
294
295 #[test]
296 fn configuration_blocking_field_omits_when_false() -> Result<(), TestError> {
297 // `blocking: false` is the default,
298 // and a field that is at its default
299 // must not be on the wire — the
300 // spec's `OpenAPI 3.x` shape does
301 // not distinguish "not present" from
302 // "present and false", but a strict
303 // peer is entitled to.
304 let config = MessageSendConfiguration::new(["text/plain"]);
305 let value = serde_json::to_value(&config)?;
306 let obj = value
307 .as_object()
308 .ok_or("a MessageSendConfiguration must serialise to a JSON object")?;
309 assert!(!obj.contains_key("blocking"));
310 Ok(())
311 }
312
313 #[test]
314 fn configuration_accepted_modes_round_trip_in_sorted_order() -> Result<(), TestError> {
315 // The `BTreeSet` gives the wire form
316 // a deterministic order (alphabetical),
317 // which keeps test fixtures readable
318 // and the diff-on-the-wire stable.
319 let config = MessageSendConfiguration::new(["text/plain", "application/json", "image/png"]);
320 let value = serde_json::to_value(&config)?;
321 let modes = value
322 .get("acceptedOutputModes")
323 .and_then(serde_json::Value::as_array)
324 .ok_or("`acceptedOutputModes` must be a JSON array")?;
325 let names: Vec<&str> = modes.iter().filter_map(serde_json::Value::as_str).collect();
326 assert_eq!(
327 names,
328 vec!["application/json", "image/png", "text/plain"],
329 "BTreeSet is sorted alphabetically"
330 );
331 Ok(())
332 }
333
334 // --- SendMessageResponse -----------------------------------------
335
336 #[test]
337 fn response_serialises_task_as_task_object() -> Result<(), TestError> {
338 let task = Task::new("task-1").with_status(TaskStatus {
339 state: TaskState::Working,
340 message: None,
341 timestamp: None,
342 });
343 let response = SendMessageResponse::Task(task);
344 let value = serde_json::to_value(&response)?;
345 let obj = value
346 .as_object()
347 .ok_or("a SendMessageResponse must serialise to a JSON object")?;
348 // The wire form is the inner Task
349 // object directly — no wrapper.
350 assert_eq!(
351 obj.get("id").and_then(serde_json::Value::as_str),
352 Some("task-1")
353 );
354 assert!(obj.contains_key("status"));
355 // A `Task` has `id`, a `Message` has
356 // `role`. The discriminator is the
357 // presence of one or the other.
358 assert!(!obj.contains_key("role"));
359 Ok(())
360 }
361
362 #[test]
363 fn response_serialises_message_as_message_object() -> Result<(), TestError> {
364 let message = Message::new(MessageRole::Agent).with_part(Part::Text(TextPart {
365 text: "Hello, user.".to_owned(),
366 metadata: std::collections::BTreeMap::new(),
367 }));
368 let response = SendMessageResponse::Message(message);
369 let value = serde_json::to_value(&response)?;
370 let obj = value
371 .as_object()
372 .ok_or("a SendMessageResponse must serialise to a JSON object")?;
373 assert_eq!(
374 obj.get("role").and_then(serde_json::Value::as_str),
375 Some("agent")
376 );
377 assert!(!obj.contains_key("id"));
378 Ok(())
379 }
380
381 #[test]
382 fn response_untagged_discrimination_works_both_ways() -> Result<(), TestError> {
383 // A `Task` JSON object decodes to
384 // `SendMessageResponse::Task`, not
385 // `::Message`. A `Message` JSON
386 // object decodes to `::Message`.
387 let task_json = r#"{"id":"task-1","status":{"state":"working"}}"#;
388 let message_json = r#"{"role":"agent","parts":[{"text":"hi"}]}"#;
389 let from_task: SendMessageResponse = serde_json::from_str(task_json)
390 .map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
391 let from_message: SendMessageResponse = serde_json::from_str(message_json)
392 .map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
393 assert!(matches!(from_task, SendMessageResponse::Task(_)));
394 assert!(matches!(from_message, SendMessageResponse::Message(_)));
395 Ok(())
396 }
397
398 #[test]
399 fn response_task_with_artifacts_round_trips() -> Result<(), TestError> {
400 let task = Task::new("task-1").with_artifact(Artifact::new(Part::Text(TextPart {
401 text: "answer".to_owned(),
402 metadata: std::collections::BTreeMap::new(),
403 })));
404 let response = SendMessageResponse::Task(task);
405 let json = serde_json::to_string(&response)?;
406 let parsed: SendMessageResponse = serde_json::from_str(&json)?;
407 assert_eq!(parsed, response);
408 Ok(())
409 }
410
411 #[test]
412 fn response_from_message_via_into_works() {
413 let message = user_message();
414 let response: SendMessageResponse = message.clone().into();
415 assert_eq!(response, SendMessageResponse::Message(message));
416 }
417}