Skip to main content

zeph_a2a/
types.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4pub enum TaskState {
5    #[serde(rename = "submitted")]
6    Submitted,
7    #[serde(rename = "working")]
8    Working,
9    #[serde(rename = "input-required")]
10    InputRequired,
11    #[serde(rename = "completed")]
12    Completed,
13    #[serde(rename = "failed")]
14    Failed,
15    #[serde(rename = "canceled")]
16    Canceled,
17    #[serde(rename = "rejected")]
18    Rejected,
19    #[serde(rename = "auth-required")]
20    AuthRequired,
21    #[serde(rename = "unknown")]
22    Unknown,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(rename_all = "camelCase")]
27pub struct Task {
28    pub id: String,
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub context_id: Option<String>,
31    pub status: TaskStatus,
32    #[serde(default, skip_serializing_if = "Vec::is_empty")]
33    pub artifacts: Vec<Artifact>,
34    #[serde(default, skip_serializing_if = "Vec::is_empty")]
35    pub history: Vec<Message>,
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    pub metadata: Option<serde_json::Value>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41#[serde(rename_all = "camelCase")]
42pub struct TaskStatus {
43    pub state: TaskState,
44    pub timestamp: String,
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub message: Option<Message>,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "lowercase")]
51pub enum Role {
52    User,
53    Agent,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct Message {
59    pub role: Role,
60    pub parts: Vec<Part>,
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub message_id: Option<String>,
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub task_id: Option<String>,
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub context_id: Option<String>,
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub metadata: Option<serde_json::Value>,
69}
70
71#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
72#[serde(tag = "kind", rename_all = "lowercase")]
73pub enum Part {
74    Text {
75        text: String,
76        #[serde(default, skip_serializing_if = "Option::is_none")]
77        metadata: Option<serde_json::Value>,
78    },
79    File {
80        file: FileContent,
81        #[serde(default, skip_serializing_if = "Option::is_none")]
82        metadata: Option<serde_json::Value>,
83    },
84    Data {
85        data: serde_json::Value,
86        #[serde(default, skip_serializing_if = "Option::is_none")]
87        metadata: Option<serde_json::Value>,
88    },
89}
90
91#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct FileContent {
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub name: Option<String>,
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub media_type: Option<String>,
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub file_with_bytes: Option<String>,
100    #[serde(default, skip_serializing_if = "Option::is_none")]
101    pub file_with_uri: Option<String>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
105#[serde(rename_all = "camelCase")]
106pub struct Artifact {
107    pub artifact_id: String,
108    #[serde(default, skip_serializing_if = "Option::is_none")]
109    pub name: Option<String>,
110    pub parts: Vec<Part>,
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    pub metadata: Option<serde_json::Value>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase")]
117pub struct AgentCard {
118    pub name: String,
119    pub description: String,
120    pub url: String,
121    pub version: String,
122    #[serde(default, skip_serializing_if = "Option::is_none")]
123    pub provider: Option<AgentProvider>,
124    pub capabilities: AgentCapabilities,
125    #[serde(default, skip_serializing_if = "Vec::is_empty")]
126    pub default_input_modes: Vec<String>,
127    #[serde(default, skip_serializing_if = "Vec::is_empty")]
128    pub default_output_modes: Vec<String>,
129    #[serde(default, skip_serializing_if = "Vec::is_empty")]
130    pub skills: Vec<AgentSkill>,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135pub struct AgentProvider {
136    pub organization: String,
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub url: Option<String>,
139}
140
141#[derive(Debug, Clone, Default, Serialize, Deserialize)]
142#[serde(rename_all = "camelCase")]
143pub struct AgentCapabilities {
144    #[serde(default)]
145    pub streaming: bool,
146    #[serde(default)]
147    pub push_notifications: bool,
148    #[serde(default)]
149    pub state_transition_history: bool,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
153#[serde(rename_all = "camelCase")]
154pub struct AgentSkill {
155    pub id: String,
156    pub name: String,
157    pub description: String,
158    #[serde(default, skip_serializing_if = "Vec::is_empty")]
159    pub tags: Vec<String>,
160    #[serde(default, skip_serializing_if = "Vec::is_empty")]
161    pub examples: Vec<String>,
162    #[serde(default, skip_serializing_if = "Vec::is_empty")]
163    pub input_modes: Vec<String>,
164    #[serde(default, skip_serializing_if = "Vec::is_empty")]
165    pub output_modes: Vec<String>,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
169#[serde(rename_all = "camelCase")]
170pub struct TaskStatusUpdateEvent {
171    #[serde(default = "kind_status_update")]
172    pub kind: String,
173    pub task_id: String,
174    #[serde(default, skip_serializing_if = "Option::is_none")]
175    pub context_id: Option<String>,
176    pub status: TaskStatus,
177    #[serde(rename = "final", default)]
178    pub is_final: bool,
179}
180
181fn kind_status_update() -> String {
182    "status-update".into()
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
186#[serde(rename_all = "camelCase")]
187pub struct TaskArtifactUpdateEvent {
188    #[serde(default = "kind_artifact_update")]
189    pub kind: String,
190    pub task_id: String,
191    #[serde(default, skip_serializing_if = "Option::is_none")]
192    pub context_id: Option<String>,
193    pub artifact: Artifact,
194    #[serde(rename = "final", default)]
195    pub is_final: bool,
196}
197
198fn kind_artifact_update() -> String {
199    "artifact-update".into()
200}
201
202impl Part {
203    #[must_use]
204    pub fn text(s: impl Into<String>) -> Self {
205        Self::Text {
206            text: s.into(),
207            metadata: None,
208        }
209    }
210}
211
212impl Message {
213    #[must_use]
214    pub fn user_text(s: impl Into<String>) -> Self {
215        Self {
216            role: Role::User,
217            parts: vec![Part::text(s)],
218            message_id: None,
219            task_id: None,
220            context_id: None,
221            metadata: None,
222        }
223    }
224
225    #[must_use]
226    pub fn text_content(&self) -> Option<&str> {
227        self.parts.iter().find_map(|p| match p {
228            Part::Text { text, .. } => Some(text.as_str()),
229            _ => None,
230        })
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn task_state_serde() {
240        let states = [
241            (TaskState::Submitted, "\"submitted\""),
242            (TaskState::Working, "\"working\""),
243            (TaskState::InputRequired, "\"input-required\""),
244            (TaskState::Completed, "\"completed\""),
245            (TaskState::Failed, "\"failed\""),
246            (TaskState::Canceled, "\"canceled\""),
247            (TaskState::Rejected, "\"rejected\""),
248            (TaskState::AuthRequired, "\"auth-required\""),
249            (TaskState::Unknown, "\"unknown\""),
250        ];
251        for (state, expected) in states {
252            let json = serde_json::to_string(&state).unwrap();
253            assert_eq!(json, expected, "serialization mismatch for {state:?}");
254            let back: TaskState = serde_json::from_str(&json).unwrap();
255            assert_eq!(back, state);
256        }
257    }
258
259    #[test]
260    fn role_serde_lowercase() {
261        assert_eq!(serde_json::to_string(&Role::User).unwrap(), "\"user\"");
262        assert_eq!(serde_json::to_string(&Role::Agent).unwrap(), "\"agent\"");
263    }
264
265    #[test]
266    fn part_text_constructor() {
267        let part = Part::text("hello");
268        assert_eq!(
269            part,
270            Part::Text {
271                text: "hello".into(),
272                metadata: None
273            }
274        );
275    }
276
277    #[test]
278    fn part_kind_serde() {
279        let text_part = Part::text("hello");
280        let json = serde_json::to_string(&text_part).unwrap();
281        assert!(json.contains("\"kind\":\"text\""));
282        assert!(json.contains("\"text\":\"hello\""));
283        let back: Part = serde_json::from_str(&json).unwrap();
284        assert_eq!(back, text_part);
285
286        let file_part = Part::File {
287            file: FileContent {
288                name: Some("doc.pdf".into()),
289                media_type: None,
290                file_with_bytes: None,
291                file_with_uri: Some("https://example.com/doc.pdf".into()),
292            },
293            metadata: None,
294        };
295        let json = serde_json::to_string(&file_part).unwrap();
296        assert!(json.contains("\"kind\":\"file\""));
297        let back: Part = serde_json::from_str(&json).unwrap();
298        assert_eq!(back, file_part);
299
300        let data_part = Part::Data {
301            data: serde_json::json!({"key": "value"}),
302            metadata: None,
303        };
304        let json = serde_json::to_string(&data_part).unwrap();
305        assert!(json.contains("\"kind\":\"data\""));
306        let back: Part = serde_json::from_str(&json).unwrap();
307        assert_eq!(back, data_part);
308    }
309
310    #[test]
311    fn message_user_text_constructor() {
312        let msg = Message::user_text("test input");
313        assert_eq!(msg.role, Role::User);
314        assert_eq!(msg.text_content(), Some("test input"));
315    }
316
317    #[test]
318    fn message_serde_round_trip() {
319        let msg = Message::user_text("hello agent");
320        let json = serde_json::to_string(&msg).unwrap();
321        let back: Message = serde_json::from_str(&json).unwrap();
322        assert_eq!(back.role, Role::User);
323        assert_eq!(back.text_content(), Some("hello agent"));
324    }
325
326    #[test]
327    fn task_serde_round_trip() {
328        let task = Task {
329            id: "task-1".into(),
330            context_id: None,
331            status: TaskStatus {
332                state: TaskState::Working,
333                timestamp: "2025-01-01T00:00:00Z".into(),
334                message: None,
335            },
336            artifacts: vec![],
337            history: vec![Message::user_text("do something")],
338            metadata: None,
339        };
340        let json = serde_json::to_string(&task).unwrap();
341        assert!(json.contains("\"contextId\"").not());
342        let back: Task = serde_json::from_str(&json).unwrap();
343        assert_eq!(back.id, "task-1");
344        assert_eq!(back.status.state, TaskState::Working);
345        assert_eq!(back.history.len(), 1);
346    }
347
348    #[test]
349    fn task_skips_empty_vecs_and_none() {
350        let task = Task {
351            id: "t".into(),
352            context_id: None,
353            status: TaskStatus {
354                state: TaskState::Submitted,
355                timestamp: "ts".into(),
356                message: None,
357            },
358            artifacts: vec![],
359            history: vec![],
360            metadata: None,
361        };
362        let json = serde_json::to_string(&task).unwrap();
363        assert!(!json.contains("artifacts"));
364        assert!(!json.contains("history"));
365        assert!(!json.contains("metadata"));
366        assert!(!json.contains("contextId"));
367    }
368
369    #[test]
370    fn artifact_serde_round_trip() {
371        let artifact = Artifact {
372            artifact_id: "art-1".into(),
373            name: Some("result.txt".into()),
374            parts: vec![Part::text("file content")],
375            metadata: None,
376        };
377        let json = serde_json::to_string(&artifact).unwrap();
378        assert!(json.contains("\"artifactId\""));
379        let back: Artifact = serde_json::from_str(&json).unwrap();
380        assert_eq!(back.artifact_id, "art-1");
381    }
382
383    #[test]
384    fn agent_card_serde_round_trip() {
385        let card = AgentCard {
386            name: "test-agent".into(),
387            description: "A test agent".into(),
388            url: "http://localhost:8080".into(),
389            version: "0.1.0".into(),
390            provider: Some(AgentProvider {
391                organization: "TestOrg".into(),
392                url: Some("https://test.org".into()),
393            }),
394            capabilities: AgentCapabilities {
395                streaming: true,
396                push_notifications: false,
397                state_transition_history: false,
398            },
399            default_input_modes: vec!["text".into()],
400            default_output_modes: vec!["text".into()],
401            skills: vec![AgentSkill {
402                id: "skill-1".into(),
403                name: "Test Skill".into(),
404                description: "Does testing".into(),
405                tags: vec!["test".into()],
406                examples: vec![],
407                input_modes: vec![],
408                output_modes: vec![],
409            }],
410        };
411        let json = serde_json::to_string_pretty(&card).unwrap();
412        let back: AgentCard = serde_json::from_str(&json).unwrap();
413        assert_eq!(back.name, "test-agent");
414        assert!(back.capabilities.streaming);
415        assert_eq!(back.skills.len(), 1);
416    }
417
418    #[test]
419    fn task_status_update_event_serde() {
420        let event = TaskStatusUpdateEvent {
421            kind: "status-update".into(),
422            task_id: "t-1".into(),
423            context_id: None,
424            status: TaskStatus {
425                state: TaskState::Completed,
426                timestamp: "ts".into(),
427                message: None,
428            },
429            is_final: true,
430        };
431        let json = serde_json::to_string(&event).unwrap();
432        assert!(json.contains("\"final\":true"));
433        assert!(!json.contains("isFinal"));
434        assert!(json.contains("\"kind\":\"status-update\""));
435        let back: TaskStatusUpdateEvent = serde_json::from_str(&json).unwrap();
436        assert!(back.is_final);
437        assert_eq!(back.kind, "status-update");
438    }
439
440    #[test]
441    fn task_artifact_update_event_serde() {
442        let event = TaskArtifactUpdateEvent {
443            kind: "artifact-update".into(),
444            task_id: "t-1".into(),
445            context_id: None,
446            artifact: Artifact {
447                artifact_id: "a-1".into(),
448                name: None,
449                parts: vec![Part::text("data")],
450                metadata: None,
451            },
452            is_final: false,
453        };
454        let json = serde_json::to_string(&event).unwrap();
455        assert!(json.contains("\"final\":false"));
456        assert!(json.contains("\"kind\":\"artifact-update\""));
457        let back: TaskArtifactUpdateEvent = serde_json::from_str(&json).unwrap();
458        assert!(!back.is_final);
459        assert_eq!(back.kind, "artifact-update");
460    }
461
462    #[test]
463    fn file_content_serde() {
464        let fc = FileContent {
465            name: Some("doc.pdf".into()),
466            media_type: Some("application/pdf".into()),
467            file_with_bytes: Some("base64data==".into()),
468            file_with_uri: None,
469        };
470        let json = serde_json::to_string(&fc).unwrap();
471        assert!(json.contains("\"mediaType\""));
472        assert!(json.contains("\"fileWithBytes\""));
473        assert!(!json.contains("fileWithUri"));
474        let back: FileContent = serde_json::from_str(&json).unwrap();
475        assert_eq!(back.name.as_deref(), Some("doc.pdf"));
476    }
477
478    trait Not {
479        fn not(&self) -> bool;
480    }
481    impl Not for bool {
482        fn not(&self) -> bool {
483            !*self
484        }
485    }
486}