1use serde::{Deserialize, Serialize};
2
3use crate::A2AError;
4use crate::types::JsonObject;
5
6use super::message::{Artifact, Message};
7use super::push::TaskPushNotificationConfig;
8use super::task::{Task, TaskStatus};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct TaskStatusUpdateEvent {
14 pub task_id: String,
16 pub context_id: String,
18 pub status: TaskStatus,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub metadata: Option<JsonObject>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct TaskArtifactUpdateEvent {
29 pub task_id: String,
31 pub context_id: String,
33 pub artifact: Artifact,
35 #[serde(default, skip_serializing_if = "crate::types::is_false")]
36 pub append: bool,
38 #[serde(default, skip_serializing_if = "crate::types::is_false")]
39 pub last_chunk: bool,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub metadata: Option<JsonObject>,
44}
45
46fn validate_task(task: &Task) -> Result<(), A2AError> {
47 for artifact in &task.artifacts {
48 artifact.validate()?;
49 }
50
51 for message in &task.history {
52 message.validate()?;
53 }
54
55 if let Some(message) = &task.status.message {
56 message.validate()?;
57 }
58
59 Ok(())
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub enum SendMessageResponse {
66 Task(Task),
68 Message(Message),
70}
71
72impl SendMessageResponse {
73 pub fn validate(&self) -> Result<(), A2AError> {
75 match self {
76 Self::Task(task) => validate_task(task),
77 Self::Message(message) => message.validate(),
78 }
79 }
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase")]
85pub enum StreamResponse {
86 Task(Task),
88 Message(Message),
90 StatusUpdate(TaskStatusUpdateEvent),
92 ArtifactUpdate(TaskArtifactUpdateEvent),
94}
95
96impl StreamResponse {
97 pub fn validate(&self) -> Result<(), A2AError> {
99 match self {
100 Self::Task(task) => validate_task(task),
101 Self::Message(message) => message.validate(),
102 Self::StatusUpdate(update) => {
103 if let Some(message) = &update.status.message {
104 message.validate()?;
105 }
106
107 Ok(())
108 }
109 Self::ArtifactUpdate(update) => update.artifact.validate(),
110 }
111 }
112}
113
114#[derive(Debug, Clone, Default, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase")]
117pub struct ListTasksResponse {
118 #[serde(default, skip_serializing_if = "Vec::is_empty")]
119 pub tasks: Vec<Task>,
121 pub next_page_token: String,
123 pub page_size: i32,
125 pub total_size: i32,
127}
128
129#[derive(Debug, Clone, Default, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase")]
132pub struct ListTaskPushNotificationConfigsResponse {
133 #[serde(default, skip_serializing_if = "Vec::is_empty")]
134 pub configs: Vec<TaskPushNotificationConfig>,
136 #[serde(default, skip_serializing_if = "String::is_empty")]
137 pub next_page_token: String,
139}
140
141#[cfg(test)]
142mod tests {
143 use super::{
144 ListTaskPushNotificationConfigsResponse, SendMessageResponse, StreamResponse,
145 TaskArtifactUpdateEvent, TaskStatusUpdateEvent,
146 };
147 use crate::types::{Artifact, Message, Part, Role, Task, TaskState, TaskStatus};
148
149 #[test]
150 fn send_message_response_uses_proto_oneof_shape() {
151 let response = SendMessageResponse::Message(Message {
152 message_id: "msg-1".to_owned(),
153 context_id: None,
154 task_id: None,
155 role: Role::Agent,
156 parts: vec![Part {
157 text: Some("done".to_owned()),
158 raw: None,
159 url: None,
160 data: None,
161 metadata: None,
162 filename: None,
163 media_type: None,
164 }],
165 metadata: None,
166 extensions: Vec::new(),
167 reference_task_ids: Vec::new(),
168 });
169
170 let json = serde_json::to_string(&response).expect("response should serialize");
171 assert_eq!(
172 json,
173 r#"{"message":{"messageId":"msg-1","role":"ROLE_AGENT","parts":[{"text":"done"}]}}"#
174 );
175 }
176
177 #[test]
178 fn send_message_response_validate_rejects_invalid_part() {
179 let response = SendMessageResponse::Message(Message {
180 message_id: "msg-1".to_owned(),
181 context_id: None,
182 task_id: None,
183 role: Role::Agent,
184 parts: vec![Part {
185 text: Some("done".to_owned()),
186 raw: Some(vec![104, 105]),
187 url: None,
188 data: None,
189 metadata: None,
190 filename: None,
191 media_type: None,
192 }],
193 metadata: None,
194 extensions: Vec::new(),
195 reference_task_ids: Vec::new(),
196 });
197
198 let error = response.validate().expect_err("response should be invalid");
199 assert!(
200 error
201 .to_string()
202 .contains("part cannot contain more than one")
203 );
204 }
205
206 #[test]
207 fn list_push_notification_response_uses_empty_string_for_no_next_page() {
208 let response = ListTaskPushNotificationConfigsResponse {
209 configs: Vec::new(),
210 next_page_token: String::new(),
211 };
212
213 let json = serde_json::to_string(&response).expect("response should serialize");
214 assert_eq!(json, "{}");
215 }
216
217 #[test]
218 fn task_status_update_event_round_trip_serialization() {
219 let event = TaskStatusUpdateEvent {
220 task_id: "task-1".to_owned(),
221 context_id: "ctx-1".to_owned(),
222 status: TaskStatus {
223 state: TaskState::Working,
224 message: Some(Message {
225 message_id: "msg-1".to_owned(),
226 context_id: Some("ctx-1".to_owned()),
227 task_id: Some("task-1".to_owned()),
228 role: Role::Agent,
229 parts: vec![Part {
230 text: Some("still working".to_owned()),
231 raw: None,
232 url: None,
233 data: None,
234 metadata: None,
235 filename: None,
236 media_type: None,
237 }],
238 metadata: None,
239 extensions: Vec::new(),
240 reference_task_ids: Vec::new(),
241 }),
242 timestamp: Some("2026-03-12T12:00:00Z".to_owned()),
243 },
244 metadata: None,
245 };
246
247 let json = serde_json::to_string(&event).expect("event should serialize");
248 let round_trip: TaskStatusUpdateEvent =
249 serde_json::from_str(&json).expect("event should deserialize");
250
251 assert_eq!(round_trip.task_id, "task-1");
252 assert_eq!(round_trip.status.state, TaskState::Working);
253 }
254
255 #[test]
256 fn task_artifact_update_event_round_trip_serialization() {
257 let event = TaskArtifactUpdateEvent {
258 task_id: "task-1".to_owned(),
259 context_id: "ctx-1".to_owned(),
260 artifact: Artifact {
261 artifact_id: "artifact-1".to_owned(),
262 name: Some("result".to_owned()),
263 description: None,
264 parts: vec![Part {
265 text: Some("partial".to_owned()),
266 raw: None,
267 url: None,
268 data: None,
269 metadata: None,
270 filename: None,
271 media_type: None,
272 }],
273 metadata: None,
274 extensions: Vec::new(),
275 },
276 append: true,
277 last_chunk: false,
278 metadata: None,
279 };
280
281 let json = serde_json::to_string(&event).expect("event should serialize");
282 let round_trip: TaskArtifactUpdateEvent =
283 serde_json::from_str(&json).expect("event should deserialize");
284
285 assert!(round_trip.append);
286 assert!(!round_trip.last_chunk);
287 assert_eq!(round_trip.artifact.artifact_id, "artifact-1");
288 }
289
290 #[test]
291 fn stream_response_round_trip_serialization() {
292 let response = StreamResponse::Task(Task {
293 id: "task-1".to_owned(),
294 context_id: Some("ctx-1".to_owned()),
295 status: TaskStatus {
296 state: TaskState::Submitted,
297 message: None,
298 timestamp: Some("2026-03-12T12:00:00Z".to_owned()),
299 },
300 artifacts: Vec::new(),
301 history: Vec::new(),
302 metadata: None,
303 });
304
305 let json = serde_json::to_string(&response).expect("response should serialize");
306 let round_trip: StreamResponse =
307 serde_json::from_str(&json).expect("response should deserialize");
308
309 match round_trip {
310 StreamResponse::Task(task) => assert_eq!(task.id, "task-1"),
311 _ => panic!("expected task stream response"),
312 }
313 }
314}