gemini_adk_rs/a2a/
types.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4/// An A2A message exchanged between agents.
5#[derive(Debug, Clone, Serialize, Deserialize)]
6#[serde(rename_all = "camelCase")]
7pub struct A2aMessage {
8    /// Unique identifier for this message.
9    pub message_id: String,
10    /// Role of the message sender ("user" or "agent").
11    pub role: String,
12    /// The content parts of the message.
13    pub parts: Vec<A2aPart>,
14    /// Optional metadata attached to the message.
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub metadata: Option<HashMap<String, serde_json::Value>>,
17}
18
19/// A2A part — discriminated by kind.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21#[serde(tag = "kind")]
22pub enum A2aPart {
23    /// A plain text part.
24    #[serde(rename = "text")]
25    Text {
26        /// The text content.
27        text: String,
28        /// Optional metadata for this part.
29        #[serde(skip_serializing_if = "Option::is_none")]
30        metadata: Option<HashMap<String, serde_json::Value>>,
31    },
32    /// A file attachment part.
33    #[serde(rename = "file")]
34    File {
35        /// The file content (inline bytes or URI).
36        file: A2aFileContent,
37        /// Optional metadata for this part.
38        #[serde(skip_serializing_if = "Option::is_none")]
39        metadata: Option<HashMap<String, serde_json::Value>>,
40    },
41    /// A structured data part (function calls, code execution, etc.).
42    #[serde(rename = "data")]
43    Data {
44        /// The structured data payload.
45        data: serde_json::Value,
46        /// Optional metadata for this part.
47        #[serde(skip_serializing_if = "Option::is_none")]
48        metadata: Option<HashMap<String, serde_json::Value>>,
49    },
50}
51
52/// File content in A2A — either inline bytes or URI reference.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub struct A2aFileContent {
56    /// File name.
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub name: Option<String>,
59    /// MIME type of the file content.
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub mime_type: Option<String>,
62    /// Inline base64-encoded bytes.
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub bytes: Option<String>,
65    /// URI reference.
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub uri: Option<String>,
68}
69
70/// Status of an A2A task.
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73pub enum TaskState {
74    /// Task has been submitted but not yet started.
75    Submitted,
76    /// Task is actively being processed.
77    Working,
78    /// Task requires additional input from the user.
79    InputRequired,
80    /// Task has completed successfully.
81    Completed,
82    /// Task was canceled by the user or system.
83    Canceled,
84    /// Task failed with an error.
85    Failed,
86    /// Task state is unknown.
87    Unknown,
88}
89
90/// A2A task status.
91#[derive(Debug, Clone, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct TaskStatus {
94    /// Current state of the task.
95    pub state: TaskState,
96    /// Optional message associated with the status.
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub message: Option<A2aMessage>,
99    /// ISO 8601 timestamp of the status update.
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub timestamp: Option<String>,
102}
103
104/// An A2A task.
105#[derive(Debug, Clone, Serialize, Deserialize)]
106#[serde(rename_all = "camelCase")]
107pub struct A2aTask {
108    /// Unique task identifier.
109    pub id: String,
110    /// Context identifier for grouping related tasks.
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub context_id: Option<String>,
113    /// Current status of the task.
114    pub status: TaskStatus,
115    /// Artifacts produced by the task.
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub artifacts: Option<Vec<A2aArtifact>>,
118    /// Optional task-level metadata.
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub metadata: Option<HashMap<String, serde_json::Value>>,
121}
122
123/// An A2A artifact.
124#[derive(Debug, Clone, Serialize, Deserialize)]
125#[serde(rename_all = "camelCase")]
126pub struct A2aArtifact {
127    /// Artifact name.
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub name: Option<String>,
130    /// Parts composing this artifact.
131    pub parts: Vec<A2aPart>,
132    /// Optional artifact-level metadata.
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub metadata: Option<HashMap<String, serde_json::Value>>,
135}
136
137/// Task status update event (streaming).
138#[derive(Debug, Clone, Serialize, Deserialize)]
139#[serde(rename_all = "camelCase")]
140pub struct TaskStatusUpdateEvent {
141    /// Task identifier.
142    pub id: String,
143    /// Updated task status.
144    pub status: TaskStatus,
145    /// Whether this is the final status update for the task.
146    #[serde(rename = "final")]
147    pub is_final: bool,
148    /// Optional event-level metadata.
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub metadata: Option<HashMap<String, serde_json::Value>>,
151}
152
153/// Task artifact update event (streaming).
154#[derive(Debug, Clone, Serialize, Deserialize)]
155#[serde(rename_all = "camelCase")]
156pub struct TaskArtifactUpdateEvent {
157    /// Task identifier.
158    pub id: String,
159    /// The updated artifact.
160    pub artifact: A2aArtifact,
161    /// Optional event-level metadata.
162    #[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        // Verify camelCase field naming
197        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        // With bytes
262        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        // With URI
273        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        // Round-trip
317        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        // Verify camelCase for contextId
361        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        // "final" is used in JSON, not "is_final" or "isFinal"
379        assert!(json.contains("\"final\""));
380        assert!(!json.contains("\"is_final\""));
381        assert!(!json.contains("\"isFinal\""));
382
383        // Deserialize from JSON with "final" key
384        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}