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 /// The configuration to send.
100 ///
101 /// Absent from the request unless this method is called.
102 #[must_use]
103 pub fn with_configuration(mut self, configuration: MessageSendConfiguration) -> Self {
104 self.configuration = Some(configuration);
105 self
106 }
107}
108
109/// Configuration for a `SendMessage` request.
110///
111/// Wire form:
112/// ```json
113/// {
114/// "acceptedOutputModes": ["text/plain", "application/json"],
115/// "historyLength": 10,
116/// "blocking": true
117/// }
118/// ```
119///
120/// All fields are optional. A configuration
121/// with every field set to its default (an
122/// empty `accepted_output_modes`, no history
123/// length, no blocking) serialises to `{}` on
124/// the wire.
125#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
126#[serde(rename_all = "camelCase")]
127pub struct MessageSendConfiguration {
128 /// The MIME types the client can accept.
129 /// A client that sends a message but the
130 /// agent's reply is in a type not in this
131 /// list is a wire contract violation; the
132 /// agent is free to refuse with a typed
133 /// error.
134 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
135 pub accepted_output_modes: BTreeSet<String>,
136 /// The number of history messages to
137 /// include in the response. A request with
138 /// no history length lets the agent choose.
139 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub history_length: Option<u32>,
141 /// Whether the request should block
142 /// until the task completes. A
143 /// non-blocking request returns a `Task`
144 /// in `Submitted` or `Working` state;
145 /// the client polls with `GetTask` to
146 /// advance.
147 #[serde(default, skip_serializing_if = "is_false")]
148 pub blocking: bool,
149}
150
151// `skip_serializing_if` requires `fn(&T) -> bool`,
152// so the argument is `&bool` even though the
153// type is one byte. The clippy lint is a false
154// positive for serde-internal helpers.
155#[expect(
156 clippy::trivially_copy_pass_by_ref,
157 reason = "required by skip_serializing_if"
158)]
159const fn is_false(b: &bool) -> bool {
160 !*b
161}
162
163impl MessageSendConfiguration {
164 /// Starts a new configuration with the
165 /// accepted output modes from `modes`. Use
166 /// [`with_history_length`](Self::with_history_length)
167 /// and [`with_blocking`](Self::with_blocking)
168 /// to set the other fields.
169 #[must_use]
170 pub fn new(modes: impl IntoIterator<Item = impl Into<String>>) -> Self {
171 Self {
172 accepted_output_modes: modes.into_iter().map(Into::into).collect(),
173 history_length: None,
174 blocking: false,
175 }
176 }
177
178 /// How much history to carry.
179 ///
180 /// Absent from the request unless this method is called, in which case
181 /// the agent keeps the history it has.
182 #[must_use]
183 pub const fn with_history_length(mut self, length: u32) -> Self {
184 self.history_length = Some(length);
185 self
186 }
187
188 /// Marks the request as blocking.
189 #[must_use]
190 pub const fn with_blocking(mut self) -> Self {
191 self.blocking = true;
192 self
193 }
194}
195
196/// A typed `SendMessage` response.
197///
198/// The wire form is the inner object directly —
199/// a `Task` is `{"id": "...", "status": ...}`,
200/// a `Message` is `{"role": ..., "parts": ...}`.
201/// The two shapes are discriminated by which
202/// top-level field is present (`id` for `Task`,
203/// `role` for `Message`).
204#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
205#[serde(untagged)]
206pub enum SendMessageResponse {
207 /// The agent created (or is creating) a
208 /// task to do the work. The task's
209 /// `status.state` is one of `Submitted`,
210 /// `Working`, `Completed`, `Failed`,
211 /// `Cancelled`, or `InputRequired`. For
212 /// a blocking request, the state is
213 /// `Completed`, `Failed`, `Cancelled`, or
214 /// `InputRequired` by the time the
215 /// response is sent.
216 Task(Task),
217 /// The agent answered directly without
218 /// creating a task. The message is the
219 /// agent's reply.
220 Message(Message),
221}
222
223impl From<Task> for SendMessageResponse {
224 fn from(task: Task) -> Self {
225 Self::Task(task)
226 }
227}
228
229impl From<Message> for SendMessageResponse {
230 fn from(message: Message) -> Self {
231 Self::Message(message)
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::{MessageSendConfiguration, SendMessageRequest, SendMessageResponse};
238 use crate::domain::a2a::message::{Message, MessageRole, Part, TextPart};
239 use crate::domain::a2a::task::{Artifact, Task, TaskState, TaskStatus};
240
241 type TestError = Box<dyn std::error::Error>;
242
243 fn user_message() -> Message {
244 Message::new(MessageRole::User).with_part(Part::Text(TextPart {
245 text: "Hello.".to_owned(),
246 metadata: std::collections::BTreeMap::new(),
247 }))
248 }
249
250 // --- SendMessageRequest ------------------------------------------
251
252 #[test]
253 fn request_serialises_with_message_field() -> Result<(), TestError> {
254 let request = SendMessageRequest::new(user_message());
255 let value = serde_json::to_value(&request)?;
256 let obj = value
257 .as_object()
258 .ok_or("a SendMessageRequest must serialise to a JSON object")?;
259 assert!(obj.contains_key("message"));
260 // The optional fields are absent on
261 // the wire when they are `None` /
262 // empty.
263 assert!(!obj.contains_key("configuration"));
264 assert!(!obj.contains_key("metadata"));
265 Ok(())
266 }
267
268 #[test]
269 fn request_round_trips_with_configuration() -> Result<(), TestError> {
270 let request = SendMessageRequest::new(user_message()).with_configuration(
271 MessageSendConfiguration::new(["text/plain", "application/json"])
272 .with_history_length(10)
273 .with_blocking(),
274 );
275 let json = serde_json::to_string(&request)?;
276 let parsed: SendMessageRequest = serde_json::from_str(&json)?;
277 assert_eq!(parsed, request);
278 Ok(())
279 }
280
281 // --- MessageSendConfiguration ------------------------------------
282
283 #[test]
284 fn configuration_with_no_fields_is_minimal() -> Result<(), TestError> {
285 // A configuration with every field
286 // at its default serialises to `{}`
287 // on the wire. The `blocking` field
288 // is `false` by default, the
289 // `accepted_output_modes` is empty,
290 // and `history_length` is `None`.
291 let config = MessageSendConfiguration::default();
292 let value = serde_json::to_value(&config)?;
293 let obj = value
294 .as_object()
295 .ok_or("a MessageSendConfiguration must serialise to a JSON object")?;
296 assert!(obj.is_empty(), "expected `{{}}`, got {value}");
297 Ok(())
298 }
299
300 #[test]
301 fn configuration_blocking_field_omits_when_false() -> Result<(), TestError> {
302 // `blocking: false` is the default,
303 // and a field that is at its default
304 // must not be on the wire — the
305 // spec's `OpenAPI 3.x` shape does
306 // not distinguish "not present" from
307 // "present and false", but a strict
308 // peer is entitled to.
309 let config = MessageSendConfiguration::new(["text/plain"]);
310 let value = serde_json::to_value(&config)?;
311 let obj = value
312 .as_object()
313 .ok_or("a MessageSendConfiguration must serialise to a JSON object")?;
314 assert!(!obj.contains_key("blocking"));
315 Ok(())
316 }
317
318 #[test]
319 fn configuration_accepted_modes_round_trip_in_sorted_order() -> Result<(), TestError> {
320 // The `BTreeSet` gives the wire form
321 // a deterministic order (alphabetical),
322 // which keeps test fixtures readable
323 // and the diff-on-the-wire stable.
324 let config = MessageSendConfiguration::new(["text/plain", "application/json", "image/png"]);
325 let value = serde_json::to_value(&config)?;
326 let modes = value
327 .get("acceptedOutputModes")
328 .and_then(serde_json::Value::as_array)
329 .ok_or("`acceptedOutputModes` must be a JSON array")?;
330 let names: Vec<&str> = modes.iter().filter_map(serde_json::Value::as_str).collect();
331 assert_eq!(
332 names,
333 vec!["application/json", "image/png", "text/plain"],
334 "BTreeSet is sorted alphabetically"
335 );
336 Ok(())
337 }
338
339 // --- SendMessageResponse -----------------------------------------
340
341 #[test]
342 fn response_serialises_task_as_task_object() -> Result<(), TestError> {
343 let task = Task::new("task-1").with_status(TaskStatus {
344 state: TaskState::Working,
345 message: None,
346 timestamp: None,
347 });
348 let response = SendMessageResponse::Task(task);
349 let value = serde_json::to_value(&response)?;
350 let obj = value
351 .as_object()
352 .ok_or("a SendMessageResponse must serialise to a JSON object")?;
353 // The wire form is the inner Task
354 // object directly — no wrapper.
355 assert_eq!(
356 obj.get("id").and_then(serde_json::Value::as_str),
357 Some("task-1")
358 );
359 assert!(obj.contains_key("status"));
360 // A `Task` has `id`, a `Message` has
361 // `role`. The discriminator is the
362 // presence of one or the other.
363 assert!(!obj.contains_key("role"));
364 Ok(())
365 }
366
367 #[test]
368 fn response_serialises_message_as_message_object() -> Result<(), TestError> {
369 let message = Message::new(MessageRole::Agent).with_part(Part::Text(TextPart {
370 text: "Hello, user.".to_owned(),
371 metadata: std::collections::BTreeMap::new(),
372 }));
373 let response = SendMessageResponse::Message(message);
374 let value = serde_json::to_value(&response)?;
375 let obj = value
376 .as_object()
377 .ok_or("a SendMessageResponse must serialise to a JSON object")?;
378 assert_eq!(
379 obj.get("role").and_then(serde_json::Value::as_str),
380 Some("agent")
381 );
382 assert!(!obj.contains_key("id"));
383 Ok(())
384 }
385
386 #[test]
387 fn response_untagged_discrimination_works_both_ways() -> Result<(), TestError> {
388 // A `Task` JSON object decodes to
389 // `SendMessageResponse::Task`, not
390 // `::Message`. A `Message` JSON
391 // object decodes to `::Message`.
392 let task_json = r#"{"id":"task-1","status":{"state":"working"}}"#;
393 let message_json = r#"{"role":"agent","parts":[{"text":"hi"}]}"#;
394 let from_task: SendMessageResponse = serde_json::from_str(task_json)
395 .map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
396 let from_message: SendMessageResponse = serde_json::from_str(message_json)
397 .map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
398 assert!(matches!(from_task, SendMessageResponse::Task(_)));
399 assert!(matches!(from_message, SendMessageResponse::Message(_)));
400 Ok(())
401 }
402
403 #[test]
404 fn response_task_with_artifacts_round_trips() -> Result<(), TestError> {
405 let task = Task::new("task-1").with_artifact(Artifact::new(Part::Text(TextPart {
406 text: "answer".to_owned(),
407 metadata: std::collections::BTreeMap::new(),
408 })));
409 let response = SendMessageResponse::Task(task);
410 let json = serde_json::to_string(&response)?;
411 let parsed: SendMessageResponse = serde_json::from_str(&json)?;
412 assert_eq!(parsed, response);
413 Ok(())
414 }
415
416 #[test]
417 fn response_from_message_via_into_works() {
418 let message = user_message();
419 let response: SendMessageResponse = message.clone().into();
420 assert_eq!(response, SendMessageResponse::Message(message));
421 }
422}