use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use super::message::Message;
use super::task::Task;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SendMessageRequest {
pub message: Message,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub configuration: Option<MessageSendConfiguration>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub metadata: BTreeMap<String, serde_json::Value>,
}
impl SendMessageRequest {
#[must_use]
pub const fn new(message: Message) -> Self {
Self {
message,
configuration: None,
metadata: BTreeMap::new(),
}
}
#[must_use]
pub fn with_configuration(mut self, configuration: MessageSendConfiguration) -> Self {
self.configuration = Some(configuration);
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MessageSendConfiguration {
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
pub accepted_output_modes: BTreeSet<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub history_length: Option<u32>,
#[serde(default, skip_serializing_if = "is_false")]
pub blocking: bool,
}
#[expect(
clippy::trivially_copy_pass_by_ref,
reason = "required by skip_serializing_if"
)]
const fn is_false(b: &bool) -> bool {
!*b
}
impl MessageSendConfiguration {
#[must_use]
pub fn new(modes: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self {
accepted_output_modes: modes.into_iter().map(Into::into).collect(),
history_length: None,
blocking: false,
}
}
#[must_use]
pub const fn with_history_length(mut self, length: u32) -> Self {
self.history_length = Some(length);
self
}
#[must_use]
pub const fn with_blocking(mut self) -> Self {
self.blocking = true;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SendMessageResponse {
Task(Task),
Message(Message),
}
impl From<Task> for SendMessageResponse {
fn from(task: Task) -> Self {
Self::Task(task)
}
}
impl From<Message> for SendMessageResponse {
fn from(message: Message) -> Self {
Self::Message(message)
}
}
#[cfg(test)]
mod tests {
use super::{MessageSendConfiguration, SendMessageRequest, SendMessageResponse};
use crate::domain::a2a::message::{Message, MessageRole, Part, TextPart};
use crate::domain::a2a::task::{Artifact, Task, TaskState, TaskStatus};
type TestError = Box<dyn std::error::Error>;
fn user_message() -> Message {
Message::new(MessageRole::User).with_part(Part::Text(TextPart {
text: "Hello.".to_owned(),
metadata: std::collections::BTreeMap::new(),
}))
}
#[test]
fn request_serialises_with_message_field() -> Result<(), TestError> {
let request = SendMessageRequest::new(user_message());
let value = serde_json::to_value(&request)?;
let obj = value
.as_object()
.ok_or("a SendMessageRequest must serialise to a JSON object")?;
assert!(obj.contains_key("message"));
assert!(!obj.contains_key("configuration"));
assert!(!obj.contains_key("metadata"));
Ok(())
}
#[test]
fn request_round_trips_with_configuration() -> Result<(), TestError> {
let request = SendMessageRequest::new(user_message()).with_configuration(
MessageSendConfiguration::new(["text/plain", "application/json"])
.with_history_length(10)
.with_blocking(),
);
let json = serde_json::to_string(&request)?;
let parsed: SendMessageRequest = serde_json::from_str(&json)?;
assert_eq!(parsed, request);
Ok(())
}
#[test]
fn configuration_with_no_fields_is_minimal() -> Result<(), TestError> {
let config = MessageSendConfiguration::default();
let value = serde_json::to_value(&config)?;
let obj = value
.as_object()
.ok_or("a MessageSendConfiguration must serialise to a JSON object")?;
assert!(obj.is_empty(), "expected `{{}}`, got {value}");
Ok(())
}
#[test]
fn configuration_blocking_field_omits_when_false() -> Result<(), TestError> {
let config = MessageSendConfiguration::new(["text/plain"]);
let value = serde_json::to_value(&config)?;
let obj = value
.as_object()
.ok_or("a MessageSendConfiguration must serialise to a JSON object")?;
assert!(!obj.contains_key("blocking"));
Ok(())
}
#[test]
fn configuration_accepted_modes_round_trip_in_sorted_order() -> Result<(), TestError> {
let config = MessageSendConfiguration::new(["text/plain", "application/json", "image/png"]);
let value = serde_json::to_value(&config)?;
let modes = value
.get("acceptedOutputModes")
.and_then(serde_json::Value::as_array)
.ok_or("`acceptedOutputModes` must be a JSON array")?;
let names: Vec<&str> = modes.iter().filter_map(serde_json::Value::as_str).collect();
assert_eq!(
names,
vec!["application/json", "image/png", "text/plain"],
"BTreeSet is sorted alphabetically"
);
Ok(())
}
#[test]
fn response_serialises_task_as_task_object() -> Result<(), TestError> {
let task = Task::new("task-1").with_status(TaskStatus {
state: TaskState::Working,
message: None,
timestamp: None,
});
let response = SendMessageResponse::Task(task);
let value = serde_json::to_value(&response)?;
let obj = value
.as_object()
.ok_or("a SendMessageResponse must serialise to a JSON object")?;
assert_eq!(
obj.get("id").and_then(serde_json::Value::as_str),
Some("task-1")
);
assert!(obj.contains_key("status"));
assert!(!obj.contains_key("role"));
Ok(())
}
#[test]
fn response_serialises_message_as_message_object() -> Result<(), TestError> {
let message = Message::new(MessageRole::Agent).with_part(Part::Text(TextPart {
text: "Hello, user.".to_owned(),
metadata: std::collections::BTreeMap::new(),
}));
let response = SendMessageResponse::Message(message);
let value = serde_json::to_value(&response)?;
let obj = value
.as_object()
.ok_or("a SendMessageResponse must serialise to a JSON object")?;
assert_eq!(
obj.get("role").and_then(serde_json::Value::as_str),
Some("agent")
);
assert!(!obj.contains_key("id"));
Ok(())
}
#[test]
fn response_untagged_discrimination_works_both_ways() -> Result<(), TestError> {
let task_json = r#"{"id":"task-1","status":{"state":"working"}}"#;
let message_json = r#"{"role":"agent","parts":[{"text":"hi"}]}"#;
let from_task: SendMessageResponse = serde_json::from_str(task_json)
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
let from_message: SendMessageResponse = serde_json::from_str(message_json)
.map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
assert!(matches!(from_task, SendMessageResponse::Task(_)));
assert!(matches!(from_message, SendMessageResponse::Message(_)));
Ok(())
}
#[test]
fn response_task_with_artifacts_round_trips() -> Result<(), TestError> {
let task = Task::new("task-1").with_artifact(Artifact::new(Part::Text(TextPart {
text: "answer".to_owned(),
metadata: std::collections::BTreeMap::new(),
})));
let response = SendMessageResponse::Task(task);
let json = serde_json::to_string(&response)?;
let parsed: SendMessageResponse = serde_json::from_str(&json)?;
assert_eq!(parsed, response);
Ok(())
}
#[test]
fn response_from_message_via_into_works() {
let message = user_message();
let response: SendMessageResponse = message.clone().into();
assert_eq!(response, SendMessageResponse::Message(message));
}
}