1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
6#[serde(rename_all = "camelCase")]
7pub struct A2aMessage {
8 pub message_id: String,
10 pub role: String,
12 pub parts: Vec<A2aPart>,
14 #[serde(skip_serializing_if = "Option::is_none")]
16 pub metadata: Option<HashMap<String, serde_json::Value>>,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21#[serde(tag = "kind")]
22pub enum A2aPart {
23 #[serde(rename = "text")]
25 Text {
26 text: String,
28 #[serde(skip_serializing_if = "Option::is_none")]
30 metadata: Option<HashMap<String, serde_json::Value>>,
31 },
32 #[serde(rename = "file")]
34 File {
35 file: A2aFileContent,
37 #[serde(skip_serializing_if = "Option::is_none")]
39 metadata: Option<HashMap<String, serde_json::Value>>,
40 },
41 #[serde(rename = "data")]
43 Data {
44 data: serde_json::Value,
46 #[serde(skip_serializing_if = "Option::is_none")]
48 metadata: Option<HashMap<String, serde_json::Value>>,
49 },
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub struct A2aFileContent {
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub name: Option<String>,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub mime_type: Option<String>,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub bytes: Option<String>,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub uri: Option<String>,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73pub enum TaskState {
74 Submitted,
76 Working,
78 InputRequired,
80 Completed,
82 Canceled,
84 Failed,
86 Unknown,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct TaskStatus {
94 pub state: TaskState,
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub message: Option<A2aMessage>,
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub timestamp: Option<String>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106#[serde(rename_all = "camelCase")]
107pub struct A2aTask {
108 pub id: String,
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub context_id: Option<String>,
113 pub status: TaskStatus,
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub artifacts: Option<Vec<A2aArtifact>>,
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub metadata: Option<HashMap<String, serde_json::Value>>,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125#[serde(rename_all = "camelCase")]
126pub struct A2aArtifact {
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub name: Option<String>,
130 pub parts: Vec<A2aPart>,
132 #[serde(skip_serializing_if = "Option::is_none")]
134 pub metadata: Option<HashMap<String, serde_json::Value>>,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139#[serde(rename_all = "camelCase")]
140pub struct TaskStatusUpdateEvent {
141 pub id: String,
143 pub status: TaskStatus,
145 #[serde(rename = "final")]
147 pub is_final: bool,
148 #[serde(skip_serializing_if = "Option::is_none")]
150 pub metadata: Option<HashMap<String, serde_json::Value>>,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
155#[serde(rename_all = "camelCase")]
156pub struct TaskArtifactUpdateEvent {
157 pub id: String,
159 pub artifact: A2aArtifact,
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub metadata: Option<HashMap<String, serde_json::Value>>,
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn a2a_message_round_trip_with_text_part() {
172 let msg = A2aMessage {
173 message_id: "msg-1".to_string(),
174 role: "user".to_string(),
175 parts: vec![A2aPart::Text {
176 text: "Hello, agent!".to_string(),
177 metadata: None,
178 }],
179 metadata: None,
180 };
181
182 let json = serde_json::to_string(&msg).unwrap();
183 let deserialized: A2aMessage = serde_json::from_str(&json).unwrap();
184
185 assert_eq!(deserialized.message_id, "msg-1");
186 assert_eq!(deserialized.role, "user");
187 assert_eq!(deserialized.parts.len(), 1);
188 match &deserialized.parts[0] {
189 A2aPart::Text { text, metadata } => {
190 assert_eq!(text, "Hello, agent!");
191 assert!(metadata.is_none());
192 }
193 _ => panic!("Expected Text part"),
194 }
195
196 assert!(json.contains("\"messageId\""));
198 assert!(!json.contains("\"message_id\""));
199 }
200
201 #[test]
202 fn a2a_part_text_serializes_with_kind_tag() {
203 let part = A2aPart::Text {
204 text: "hello".to_string(),
205 metadata: None,
206 };
207 let json = serde_json::to_string(&part).unwrap();
208 assert!(json.contains("\"kind\":\"text\""));
209 assert!(json.contains("\"text\":\"hello\""));
210 }
211
212 #[test]
213 fn a2a_part_file_serializes_with_kind_tag() {
214 let part = A2aPart::File {
215 file: A2aFileContent {
216 name: Some("image.png".to_string()),
217 mime_type: Some("image/png".to_string()),
218 bytes: Some("base64data".to_string()),
219 uri: None,
220 },
221 metadata: None,
222 };
223 let json = serde_json::to_string(&part).unwrap();
224 assert!(json.contains("\"kind\":\"file\""));
225 assert!(json.contains("\"image.png\""));
226 assert!(json.contains("\"mimeType\""));
227
228 let deserialized: A2aPart = serde_json::from_str(&json).unwrap();
229 match deserialized {
230 A2aPart::File { file, .. } => {
231 assert_eq!(file.name.as_deref(), Some("image.png"));
232 assert_eq!(file.mime_type.as_deref(), Some("image/png"));
233 assert_eq!(file.bytes.as_deref(), Some("base64data"));
234 assert!(file.uri.is_none());
235 }
236 _ => panic!("Expected File part"),
237 }
238 }
239
240 #[test]
241 fn a2a_part_data_serializes_with_kind_tag() {
242 let part = A2aPart::Data {
243 data: serde_json::json!({"key": "value", "count": 42}),
244 metadata: None,
245 };
246 let json = serde_json::to_string(&part).unwrap();
247 assert!(json.contains("\"kind\":\"data\""));
248
249 let deserialized: A2aPart = serde_json::from_str(&json).unwrap();
250 match deserialized {
251 A2aPart::Data { data, .. } => {
252 assert_eq!(data["key"], "value");
253 assert_eq!(data["count"], 42);
254 }
255 _ => panic!("Expected Data part"),
256 }
257 }
258
259 #[test]
260 fn a2a_file_content_with_bytes_vs_uri() {
261 let with_bytes = A2aFileContent {
263 name: Some("doc.pdf".to_string()),
264 mime_type: Some("application/pdf".to_string()),
265 bytes: Some("cGRmY29udGVudA==".to_string()),
266 uri: None,
267 };
268 let json = serde_json::to_string(&with_bytes).unwrap();
269 assert!(json.contains("\"bytes\""));
270 assert!(!json.contains("\"uri\""));
271
272 let with_uri = A2aFileContent {
274 name: Some("doc.pdf".to_string()),
275 mime_type: Some("application/pdf".to_string()),
276 bytes: None,
277 uri: Some("gs://bucket/doc.pdf".to_string()),
278 };
279 let json = serde_json::to_string(&with_uri).unwrap();
280 assert!(!json.contains("\"bytes\""));
281 assert!(json.contains("\"uri\""));
282 assert!(json.contains("gs://bucket/doc.pdf"));
283 }
284
285 #[test]
286 fn task_state_serde_camel_case() {
287 assert_eq!(
288 serde_json::to_string(&TaskState::Submitted).unwrap(),
289 "\"submitted\""
290 );
291 assert_eq!(
292 serde_json::to_string(&TaskState::Working).unwrap(),
293 "\"working\""
294 );
295 assert_eq!(
296 serde_json::to_string(&TaskState::InputRequired).unwrap(),
297 "\"inputRequired\""
298 );
299 assert_eq!(
300 serde_json::to_string(&TaskState::Completed).unwrap(),
301 "\"completed\""
302 );
303 assert_eq!(
304 serde_json::to_string(&TaskState::Canceled).unwrap(),
305 "\"canceled\""
306 );
307 assert_eq!(
308 serde_json::to_string(&TaskState::Failed).unwrap(),
309 "\"failed\""
310 );
311 assert_eq!(
312 serde_json::to_string(&TaskState::Unknown).unwrap(),
313 "\"unknown\""
314 );
315
316 let state: TaskState = serde_json::from_str("\"inputRequired\"").unwrap();
318 assert_eq!(state, TaskState::InputRequired);
319 }
320
321 #[test]
322 fn a2a_task_with_status_and_artifacts() {
323 let task = A2aTask {
324 id: "task-123".to_string(),
325 context_id: Some("ctx-456".to_string()),
326 status: TaskStatus {
327 state: TaskState::Completed,
328 message: Some(A2aMessage {
329 message_id: "msg-done".to_string(),
330 role: "agent".to_string(),
331 parts: vec![A2aPart::Text {
332 text: "Done!".to_string(),
333 metadata: None,
334 }],
335 metadata: None,
336 }),
337 timestamp: Some("2026-03-02T12:00:00Z".to_string()),
338 },
339 artifacts: Some(vec![A2aArtifact {
340 name: Some("result".to_string()),
341 parts: vec![A2aPart::Data {
342 data: serde_json::json!({"answer": 42}),
343 metadata: None,
344 }],
345 metadata: None,
346 }]),
347 metadata: None,
348 };
349
350 let json = serde_json::to_string_pretty(&task).unwrap();
351 let deserialized: A2aTask = serde_json::from_str(&json).unwrap();
352
353 assert_eq!(deserialized.id, "task-123");
354 assert_eq!(deserialized.context_id.as_deref(), Some("ctx-456"));
355 assert_eq!(deserialized.status.state, TaskState::Completed);
356 assert!(deserialized.status.message.is_some());
357 assert!(deserialized.artifacts.is_some());
358 assert_eq!(deserialized.artifacts.as_ref().unwrap().len(), 1);
359
360 assert!(json.contains("\"contextId\""));
362 }
363
364 #[test]
365 fn task_status_update_event_final_field_rename() {
366 let event = TaskStatusUpdateEvent {
367 id: "task-789".to_string(),
368 status: TaskStatus {
369 state: TaskState::Working,
370 message: None,
371 timestamp: None,
372 },
373 is_final: false,
374 metadata: None,
375 };
376
377 let json = serde_json::to_string(&event).unwrap();
378 assert!(json.contains("\"final\""));
380 assert!(!json.contains("\"is_final\""));
381 assert!(!json.contains("\"isFinal\""));
382
383 let json_input = r#"{"id":"task-789","status":{"state":"completed"},"final":true}"#;
385 let deserialized: TaskStatusUpdateEvent = serde_json::from_str(json_input).unwrap();
386 assert!(deserialized.is_final);
387 assert_eq!(deserialized.status.state, TaskState::Completed);
388 }
389
390 #[test]
391 fn task_artifact_update_event_round_trip() {
392 let event = TaskArtifactUpdateEvent {
393 id: "task-100".to_string(),
394 artifact: A2aArtifact {
395 name: Some("output".to_string()),
396 parts: vec![A2aPart::Text {
397 text: "Generated content".to_string(),
398 metadata: None,
399 }],
400 metadata: None,
401 },
402 metadata: Some({
403 let mut m = HashMap::new();
404 m.insert("source".to_string(), serde_json::json!("agent-a"));
405 m
406 }),
407 };
408
409 let json = serde_json::to_string(&event).unwrap();
410 let deserialized: TaskArtifactUpdateEvent = serde_json::from_str(&json).unwrap();
411
412 assert_eq!(deserialized.id, "task-100");
413 assert_eq!(deserialized.artifact.name.as_deref(), Some("output"));
414 assert!(deserialized.metadata.is_some());
415 assert_eq!(
416 deserialized.metadata.as_ref().unwrap()["source"],
417 serde_json::json!("agent-a")
418 );
419 }
420}