Skip to main content

zeph_a2a/
types.rs

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