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    pub protocol_version: String,
126    #[serde(default, skip_serializing_if = "Option::is_none")]
127    pub provider: Option<AgentProvider>,
128    pub capabilities: AgentCapabilities,
129    #[serde(default, skip_serializing_if = "Vec::is_empty")]
130    pub default_input_modes: Vec<String>,
131    #[serde(default, skip_serializing_if = "Vec::is_empty")]
132    pub default_output_modes: Vec<String>,
133    #[serde(default, skip_serializing_if = "Vec::is_empty")]
134    pub skills: Vec<AgentSkill>,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
138#[serde(rename_all = "camelCase")]
139pub struct AgentProvider {
140    pub organization: String,
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub url: Option<String>,
143}
144
145#[derive(Debug, Clone, Default, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase")]
147pub struct AgentCapabilities {
148    #[serde(default)]
149    pub streaming: bool,
150    #[serde(default)]
151    pub push_notifications: bool,
152    #[serde(default)]
153    pub state_transition_history: bool,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
157#[serde(rename_all = "camelCase")]
158pub struct AgentSkill {
159    pub id: String,
160    pub name: String,
161    pub description: String,
162    #[serde(default, skip_serializing_if = "Vec::is_empty")]
163    pub tags: Vec<String>,
164    #[serde(default, skip_serializing_if = "Vec::is_empty")]
165    pub examples: Vec<String>,
166    #[serde(default, skip_serializing_if = "Vec::is_empty")]
167    pub input_modes: Vec<String>,
168    #[serde(default, skip_serializing_if = "Vec::is_empty")]
169    pub output_modes: Vec<String>,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
173#[serde(rename_all = "camelCase")]
174pub struct TaskStatusUpdateEvent {
175    #[serde(default = "kind_status_update")]
176    pub kind: String,
177    pub task_id: String,
178    #[serde(default, skip_serializing_if = "Option::is_none")]
179    pub context_id: Option<String>,
180    pub status: TaskStatus,
181    #[serde(rename = "final", default)]
182    pub is_final: bool,
183}
184
185fn kind_status_update() -> String {
186    "status-update".into()
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
190#[serde(rename_all = "camelCase")]
191pub struct TaskArtifactUpdateEvent {
192    #[serde(default = "kind_artifact_update")]
193    pub kind: String,
194    pub task_id: String,
195    #[serde(default, skip_serializing_if = "Option::is_none")]
196    pub context_id: Option<String>,
197    pub artifact: Artifact,
198    #[serde(rename = "final", default)]
199    pub is_final: bool,
200}
201
202fn kind_artifact_update() -> String {
203    "artifact-update".into()
204}
205
206impl Part {
207    #[must_use]
208    pub fn text(s: impl Into<String>) -> Self {
209        Self::Text {
210            text: s.into(),
211            metadata: None,
212        }
213    }
214}
215
216impl Message {
217    #[must_use]
218    pub fn user_text(s: impl Into<String>) -> Self {
219        Self {
220            role: Role::User,
221            parts: vec![Part::text(s)],
222            message_id: None,
223            task_id: None,
224            context_id: None,
225            metadata: None,
226        }
227    }
228
229    #[must_use]
230    pub fn text_content(&self) -> Option<&str> {
231        self.parts.iter().find_map(|p| match p {
232            Part::Text { text, .. } => Some(text.as_str()),
233            _ => None,
234        })
235    }
236
237    /// Collect and concatenate all `Part::Text` entries in order.
238    ///
239    /// Unlike [`text_content`] which returns only the first text part, this method
240    /// preserves the full message when an agent sends multiple text parts.
241    /// Returns an empty string if the message contains no text parts.
242    #[must_use]
243    pub fn all_text_content(&self) -> String {
244        let parts: Vec<&str> = self
245            .parts
246            .iter()
247            .filter_map(|p| match p {
248                Part::Text { text, .. } => Some(text.as_str()),
249                _ => None,
250            })
251            .collect();
252        parts.join("\n\n")
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn task_state_serde() {
262        let states = [
263            (TaskState::Submitted, "\"submitted\""),
264            (TaskState::Working, "\"working\""),
265            (TaskState::InputRequired, "\"input-required\""),
266            (TaskState::Completed, "\"completed\""),
267            (TaskState::Failed, "\"failed\""),
268            (TaskState::Canceled, "\"canceled\""),
269            (TaskState::Rejected, "\"rejected\""),
270            (TaskState::AuthRequired, "\"auth-required\""),
271            (TaskState::Unknown, "\"unknown\""),
272        ];
273        for (state, expected) in states {
274            let json = serde_json::to_string(&state).unwrap();
275            assert_eq!(json, expected, "serialization mismatch for {state:?}");
276            let back: TaskState = serde_json::from_str(&json).unwrap();
277            assert_eq!(back, state);
278        }
279    }
280
281    #[test]
282    fn role_serde_lowercase() {
283        assert_eq!(serde_json::to_string(&Role::User).unwrap(), "\"user\"");
284        assert_eq!(serde_json::to_string(&Role::Agent).unwrap(), "\"agent\"");
285    }
286
287    #[test]
288    fn part_text_constructor() {
289        let part = Part::text("hello");
290        assert_eq!(
291            part,
292            Part::Text {
293                text: "hello".into(),
294                metadata: None
295            }
296        );
297    }
298
299    #[test]
300    fn part_kind_serde() {
301        let text_part = Part::text("hello");
302        let json = serde_json::to_string(&text_part).unwrap();
303        assert!(json.contains("\"kind\":\"text\""));
304        assert!(json.contains("\"text\":\"hello\""));
305        let back: Part = serde_json::from_str(&json).unwrap();
306        assert_eq!(back, text_part);
307
308        let file_part = Part::File {
309            file: FileContent {
310                name: Some("doc.pdf".into()),
311                media_type: None,
312                file_with_bytes: None,
313                file_with_uri: Some("https://example.com/doc.pdf".into()),
314            },
315            metadata: None,
316        };
317        let json = serde_json::to_string(&file_part).unwrap();
318        assert!(json.contains("\"kind\":\"file\""));
319        let back: Part = serde_json::from_str(&json).unwrap();
320        assert_eq!(back, file_part);
321
322        let data_part = Part::Data {
323            data: serde_json::json!({"key": "value"}),
324            metadata: None,
325        };
326        let json = serde_json::to_string(&data_part).unwrap();
327        assert!(json.contains("\"kind\":\"data\""));
328        let back: Part = serde_json::from_str(&json).unwrap();
329        assert_eq!(back, data_part);
330    }
331
332    #[test]
333    fn message_user_text_constructor() {
334        let msg = Message::user_text("test input");
335        assert_eq!(msg.role, Role::User);
336        assert_eq!(msg.text_content(), Some("test input"));
337    }
338
339    #[test]
340    fn message_serde_round_trip() {
341        let msg = Message::user_text("hello agent");
342        let json = serde_json::to_string(&msg).unwrap();
343        let back: Message = serde_json::from_str(&json).unwrap();
344        assert_eq!(back.role, Role::User);
345        assert_eq!(back.text_content(), Some("hello agent"));
346    }
347
348    #[test]
349    fn task_serde_round_trip() {
350        let task = Task {
351            id: "task-1".into(),
352            context_id: None,
353            status: TaskStatus {
354                state: TaskState::Working,
355                timestamp: "2025-01-01T00:00:00Z".into(),
356                message: None,
357            },
358            artifacts: vec![],
359            history: vec![Message::user_text("do something")],
360            metadata: None,
361        };
362        let json = serde_json::to_string(&task).unwrap();
363        assert!(json.contains("\"contextId\"").not());
364        let back: Task = serde_json::from_str(&json).unwrap();
365        assert_eq!(back.id, "task-1");
366        assert_eq!(back.status.state, TaskState::Working);
367        assert_eq!(back.history.len(), 1);
368    }
369
370    #[test]
371    fn task_skips_empty_vecs_and_none() {
372        let task = Task {
373            id: "t".into(),
374            context_id: None,
375            status: TaskStatus {
376                state: TaskState::Submitted,
377                timestamp: "ts".into(),
378                message: None,
379            },
380            artifacts: vec![],
381            history: vec![],
382            metadata: None,
383        };
384        let json = serde_json::to_string(&task).unwrap();
385        assert!(!json.contains("artifacts"));
386        assert!(!json.contains("history"));
387        assert!(!json.contains("metadata"));
388        assert!(!json.contains("contextId"));
389    }
390
391    #[test]
392    fn artifact_serde_round_trip() {
393        let artifact = Artifact {
394            artifact_id: "art-1".into(),
395            name: Some("result.txt".into()),
396            parts: vec![Part::text("file content")],
397            metadata: None,
398        };
399        let json = serde_json::to_string(&artifact).unwrap();
400        assert!(json.contains("\"artifactId\""));
401        let back: Artifact = serde_json::from_str(&json).unwrap();
402        assert_eq!(back.artifact_id, "art-1");
403    }
404
405    #[test]
406    fn agent_card_serde_round_trip() {
407        let card = AgentCard {
408            name: "test-agent".into(),
409            description: "A test agent".into(),
410            url: "http://localhost:8080".into(),
411            version: "0.1.0".into(),
412            protocol_version: "0.2.1".into(),
413            provider: Some(AgentProvider {
414                organization: "TestOrg".into(),
415                url: Some("https://test.org".into()),
416            }),
417            capabilities: AgentCapabilities {
418                streaming: true,
419                push_notifications: false,
420                state_transition_history: false,
421            },
422            default_input_modes: vec!["text".into()],
423            default_output_modes: vec!["text".into()],
424            skills: vec![AgentSkill {
425                id: "skill-1".into(),
426                name: "Test Skill".into(),
427                description: "Does testing".into(),
428                tags: vec!["test".into()],
429                examples: vec![],
430                input_modes: vec![],
431                output_modes: vec![],
432            }],
433        };
434        let json = serde_json::to_string_pretty(&card).unwrap();
435        let back: AgentCard = serde_json::from_str(&json).unwrap();
436        assert_eq!(back.name, "test-agent");
437        assert!(back.capabilities.streaming);
438        assert_eq!(back.skills.len(), 1);
439    }
440
441    #[test]
442    fn task_status_update_event_serde() {
443        let event = TaskStatusUpdateEvent {
444            kind: "status-update".into(),
445            task_id: "t-1".into(),
446            context_id: None,
447            status: TaskStatus {
448                state: TaskState::Completed,
449                timestamp: "ts".into(),
450                message: None,
451            },
452            is_final: true,
453        };
454        let json = serde_json::to_string(&event).unwrap();
455        assert!(json.contains("\"final\":true"));
456        assert!(!json.contains("isFinal"));
457        assert!(json.contains("\"kind\":\"status-update\""));
458        let back: TaskStatusUpdateEvent = serde_json::from_str(&json).unwrap();
459        assert!(back.is_final);
460        assert_eq!(back.kind, "status-update");
461    }
462
463    #[test]
464    fn task_artifact_update_event_serde() {
465        let event = TaskArtifactUpdateEvent {
466            kind: "artifact-update".into(),
467            task_id: "t-1".into(),
468            context_id: None,
469            artifact: Artifact {
470                artifact_id: "a-1".into(),
471                name: None,
472                parts: vec![Part::text("data")],
473                metadata: None,
474            },
475            is_final: false,
476        };
477        let json = serde_json::to_string(&event).unwrap();
478        assert!(json.contains("\"final\":false"));
479        assert!(json.contains("\"kind\":\"artifact-update\""));
480        let back: TaskArtifactUpdateEvent = serde_json::from_str(&json).unwrap();
481        assert!(!back.is_final);
482        assert_eq!(back.kind, "artifact-update");
483    }
484
485    #[test]
486    fn file_content_serde() {
487        let fc = FileContent {
488            name: Some("doc.pdf".into()),
489            media_type: Some("application/pdf".into()),
490            file_with_bytes: Some("base64data==".into()),
491            file_with_uri: None,
492        };
493        let json = serde_json::to_string(&fc).unwrap();
494        assert!(json.contains("\"mediaType\""));
495        assert!(json.contains("\"fileWithBytes\""));
496        assert!(!json.contains("fileWithUri"));
497        let back: FileContent = serde_json::from_str(&json).unwrap();
498        assert_eq!(back.name.as_deref(), Some("doc.pdf"));
499    }
500
501    #[test]
502    fn all_text_content_single_part() {
503        let msg = Message::user_text("hello world");
504        assert_eq!(msg.all_text_content(), "hello world");
505    }
506
507    #[test]
508    fn all_text_content_multiple_parts_joined() {
509        let msg = Message {
510            role: Role::User,
511            parts: vec![
512                Part::text("first"),
513                Part::text("second"),
514                Part::text("third"),
515            ],
516            message_id: None,
517            task_id: None,
518            context_id: None,
519            metadata: None,
520        };
521        assert_eq!(msg.all_text_content(), "first\n\nsecond\n\nthird");
522    }
523
524    #[test]
525    fn all_text_content_no_text_parts_returns_empty() {
526        let msg = Message {
527            role: Role::User,
528            parts: vec![],
529            message_id: None,
530            task_id: None,
531            context_id: None,
532            metadata: None,
533        };
534        assert_eq!(msg.all_text_content(), "");
535    }
536
537    #[test]
538    fn all_text_content_skips_non_text_parts() {
539        let msg = Message {
540            role: Role::User,
541            parts: vec![
542                Part::text("text-only"),
543                Part::Data {
544                    data: serde_json::json!({"key": "val"}),
545                    metadata: None,
546                },
547            ],
548            message_id: None,
549            task_id: None,
550            context_id: None,
551            metadata: None,
552        };
553        assert_eq!(msg.all_text_content(), "text-only");
554    }
555
556    trait Not {
557        fn not(&self) -> bool;
558    }
559    impl Not for bool {
560        fn not(&self) -> bool {
561            !*self
562        }
563    }
564}