Skip to main content

mika_a2a/
types.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4/// Role of a message sender.
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
6#[serde(rename_all = "lowercase")]
7pub enum Role {
8    User,
9    Agent,
10}
11
12/// File content - either inline bytes (base64) or a URL reference.
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14#[serde(rename_all = "camelCase")]
15pub struct FileContent {
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub name: Option<String>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub mime_type: Option<String>,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub bytes: Option<String>,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub url: Option<String>,
24}
25
26/// A content part within a message or artifact.
27#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
28#[serde(tag = "kind", rename_all = "lowercase")]
29pub enum Part {
30    #[serde(rename_all = "camelCase")]
31    Text {
32        text: String,
33        #[serde(skip_serializing_if = "Option::is_none")]
34        metadata: Option<HashMap<String, serde_json::Value>>,
35    },
36    #[serde(rename_all = "camelCase")]
37    File {
38        file: FileContent,
39        #[serde(skip_serializing_if = "Option::is_none")]
40        metadata: Option<HashMap<String, serde_json::Value>>,
41    },
42    #[serde(rename_all = "camelCase")]
43    Data {
44        data: serde_json::Value,
45        #[serde(skip_serializing_if = "Option::is_none")]
46        metadata: Option<HashMap<String, serde_json::Value>>,
47    },
48}
49
50/// Task state in the A2A state machine.
51#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
52#[serde(rename_all = "kebab-case")]
53pub enum TaskState {
54    Unknown,
55    Submitted,
56    Working,
57    InputRequired,
58    AuthRequired,
59    Completed,
60    Failed,
61    Canceled,
62    Rejected,
63}
64
65impl std::fmt::Display for TaskState {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        let s = match self {
68            Self::Unknown => "unknown",
69            Self::Submitted => "submitted",
70            Self::Working => "working",
71            Self::InputRequired => "input-required",
72            Self::AuthRequired => "auth-required",
73            Self::Completed => "completed",
74            Self::Failed => "failed",
75            Self::Canceled => "canceled",
76            Self::Rejected => "rejected",
77        };
78        write!(f, "{s}")
79    }
80}
81
82/// Current status of a task.
83#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
84#[serde(rename_all = "camelCase")]
85pub struct TaskStatus {
86    pub state: TaskState,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub message: Option<Message>,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub timestamp: Option<String>,
91}
92
93/// A message in the A2A protocol.
94#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
95#[serde(rename_all = "camelCase")]
96pub struct Message {
97    pub message_id: String,
98    pub role: Role,
99    pub parts: Vec<Part>,
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub context_id: Option<String>,
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub task_id: Option<String>,
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub metadata: Option<HashMap<String, serde_json::Value>>,
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub reference_task_ids: Option<Vec<String>>,
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub extensions: Option<Vec<String>>,
110    #[serde(default = "message_kind")]
111    pub kind: String,
112}
113
114fn message_kind() -> String {
115    "message".to_string()
116}
117
118/// An artifact produced by an agent during task processing.
119#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
120#[serde(rename_all = "camelCase")]
121pub struct Artifact {
122    pub artifact_id: String,
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub name: Option<String>,
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub description: Option<String>,
127    pub parts: Vec<Part>,
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub metadata: Option<HashMap<String, serde_json::Value>>,
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub extensions: Option<Vec<String>>,
132}
133
134/// A task in the A2A protocol.
135#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
136#[serde(rename_all = "camelCase")]
137pub struct Task {
138    pub id: String,
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub context_id: Option<String>,
141    pub status: TaskStatus,
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub artifacts: Option<Vec<Artifact>>,
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub history: Option<Vec<Message>>,
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub metadata: Option<HashMap<String, serde_json::Value>>,
148    #[serde(default = "task_kind")]
149    pub kind: String,
150}
151
152fn task_kind() -> String {
153    "task".to_string()
154}
155
156/// Capabilities advertised by an agent.
157#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
158#[serde(rename_all = "camelCase")]
159pub struct AgentCapabilities {
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub streaming: Option<bool>,
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub push_notifications: Option<bool>,
164}
165
166/// Agent provider information.
167#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
168#[serde(rename_all = "camelCase")]
169pub struct AgentProvider {
170    pub organization: String,
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub url: Option<String>,
173}
174
175/// A skill advertised by an agent.
176#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
177#[serde(rename_all = "camelCase")]
178pub struct AgentSkill {
179    pub id: String,
180    pub name: String,
181    pub description: String,
182    pub tags: Vec<String>,
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub examples: Option<Vec<String>>,
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub input_modes: Option<Vec<String>>,
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub output_modes: Option<Vec<String>>,
189}
190
191/// Security scheme for agent authentication.
192#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
193#[serde(tag = "type", rename_all = "camelCase")]
194pub enum SecurityScheme {
195    #[serde(rename = "apiKey")]
196    ApiKey {
197        name: String,
198        #[serde(rename = "in")]
199        location: String,
200    },
201    #[serde(rename = "http")]
202    Http {
203        scheme: String,
204        #[serde(skip_serializing_if = "Option::is_none")]
205        bearer_format: Option<String>,
206    },
207    #[serde(rename = "oauth2")]
208    OAuth2 { flows: serde_json::Value },
209}
210
211/// Agent Card — the public discovery document for an A2A agent.
212#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
213#[serde(rename_all = "camelCase")]
214pub struct AgentCard {
215    pub name: String,
216    pub description: String,
217    pub version: String,
218    pub url: String,
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub provider: Option<AgentProvider>,
221    pub capabilities: AgentCapabilities,
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub security_schemes: Option<HashMap<String, SecurityScheme>>,
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub security_requirements: Option<Vec<HashMap<String, Vec<String>>>>,
226    pub default_input_modes: Vec<String>,
227    pub default_output_modes: Vec<String>,
228    pub skills: Vec<AgentSkill>,
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub icon_url: Option<String>,
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub documentation_url: Option<String>,
233}
234
235/// Push notification authentication.
236#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
237#[serde(rename_all = "camelCase")]
238pub struct AuthenticationInfo {
239    pub scheme: String,
240    pub credentials: String,
241}
242
243/// Push notification configuration for a task.
244#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
245#[serde(rename_all = "camelCase")]
246pub struct TaskPushNotificationConfig {
247    pub id: String,
248    pub task_id: String,
249    pub url: String,
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub token: Option<String>,
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub authentication: Option<AuthenticationInfo>,
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn part_text_round_trip() {
262        let part = Part::Text {
263            text: "hello".to_string(),
264            metadata: None,
265        };
266        let json = serde_json::to_value(&part).unwrap();
267        assert_eq!(json["kind"], "text");
268        assert_eq!(json["text"], "hello");
269        let deserialized: Part = serde_json::from_value(json).unwrap();
270        assert_eq!(deserialized, part);
271    }
272
273    #[test]
274    fn part_text_with_metadata_round_trip() {
275        let mut meta = HashMap::new();
276        meta.insert("key".to_string(), serde_json::json!("value"));
277        let part = Part::Text {
278            text: "hi".to_string(),
279            metadata: Some(meta),
280        };
281        let json = serde_json::to_value(&part).unwrap();
282        assert_eq!(json["kind"], "text");
283        assert_eq!(json["metadata"]["key"], "value");
284        let deserialized: Part = serde_json::from_value(json).unwrap();
285        assert_eq!(deserialized, part);
286    }
287
288    #[test]
289    fn part_file_round_trip() {
290        let part = Part::File {
291            file: FileContent {
292                name: Some("test.txt".to_string()),
293                mime_type: Some("text/plain".to_string()),
294                bytes: Some("aGVsbG8=".to_string()),
295                url: None,
296            },
297            metadata: None,
298        };
299        let json = serde_json::to_value(&part).unwrap();
300        assert_eq!(json["kind"], "file");
301        assert_eq!(json["file"]["name"], "test.txt");
302        assert_eq!(json["file"]["mimeType"], "text/plain");
303        let deserialized: Part = serde_json::from_value(json).unwrap();
304        assert_eq!(deserialized, part);
305    }
306
307    #[test]
308    fn part_file_url_round_trip() {
309        let part = Part::File {
310            file: FileContent {
311                name: None,
312                mime_type: None,
313                bytes: None,
314                url: Some("https://example.com/file.pdf".to_string()),
315            },
316            metadata: None,
317        };
318        let json = serde_json::to_value(&part).unwrap();
319        assert_eq!(json["kind"], "file");
320        assert_eq!(json["file"]["url"], "https://example.com/file.pdf");
321        // Optional fields should be absent
322        assert!(json["file"].get("name").is_none());
323        let deserialized: Part = serde_json::from_value(json).unwrap();
324        assert_eq!(deserialized, part);
325    }
326
327    #[test]
328    fn part_data_round_trip() {
329        let part = Part::Data {
330            data: serde_json::json!({"numbers": [1, 2, 3]}),
331            metadata: None,
332        };
333        let json = serde_json::to_value(&part).unwrap();
334        assert_eq!(json["kind"], "data");
335        assert_eq!(json["data"]["numbers"], serde_json::json!([1, 2, 3]));
336        let deserialized: Part = serde_json::from_value(json).unwrap();
337        assert_eq!(deserialized, part);
338    }
339
340    #[test]
341    fn message_round_trip() {
342        let msg = Message {
343            message_id: "msg-1".to_string(),
344            role: Role::User,
345            parts: vec![Part::Text {
346                text: "hello agent".to_string(),
347                metadata: None,
348            }],
349            context_id: Some("ctx-1".to_string()),
350            task_id: None,
351            metadata: None,
352            reference_task_ids: None,
353            extensions: None,
354            kind: "message".to_string(),
355        };
356        let json = serde_json::to_string(&msg).unwrap();
357        let deserialized: Message = serde_json::from_str(&json).unwrap();
358        assert_eq!(deserialized, msg);
359    }
360
361    #[test]
362    fn message_role_serialization() {
363        let json = serde_json::to_value(Role::User).unwrap();
364        assert_eq!(json, "user");
365        let json = serde_json::to_value(Role::Agent).unwrap();
366        assert_eq!(json, "agent");
367    }
368
369    #[test]
370    fn message_kind_defaults() {
371        // When kind is missing from JSON, it should default to "message"
372        let json = serde_json::json!({
373            "messageId": "m1",
374            "role": "agent",
375            "parts": [{"kind": "text", "text": "hi"}]
376        });
377        let msg: Message = serde_json::from_value(json).unwrap();
378        assert_eq!(msg.kind, "message");
379    }
380
381    #[test]
382    fn task_round_trip() {
383        let task = Task {
384            id: "task-1".to_string(),
385            context_id: Some("ctx-1".to_string()),
386            status: TaskStatus {
387                state: TaskState::Working,
388                message: None,
389                timestamp: Some("2025-01-01T00:00:00Z".to_string()),
390            },
391            artifacts: None,
392            history: Some(vec![Message {
393                message_id: "msg-1".to_string(),
394                role: Role::User,
395                parts: vec![Part::Text {
396                    text: "do something".to_string(),
397                    metadata: None,
398                }],
399                context_id: None,
400                task_id: Some("task-1".to_string()),
401                metadata: None,
402                reference_task_ids: None,
403                extensions: None,
404                kind: "message".to_string(),
405            }]),
406            metadata: None,
407            kind: "task".to_string(),
408        };
409        let json = serde_json::to_string(&task).unwrap();
410        let deserialized: Task =
411            serde_json::from_value(serde_json::from_str(&json).unwrap()).unwrap();
412        assert_eq!(deserialized, task);
413    }
414
415    #[test]
416    fn task_kind_defaults() {
417        let json = serde_json::json!({
418            "id": "t1",
419            "status": {"state": "submitted"}
420        });
421        let task: Task = serde_json::from_value(json).unwrap();
422        assert_eq!(task.kind, "task");
423    }
424
425    #[test]
426    fn task_state_kebab_case_serialization() {
427        let cases = [
428            (TaskState::Unknown, "unknown"),
429            (TaskState::Submitted, "submitted"),
430            (TaskState::Working, "working"),
431            (TaskState::InputRequired, "input-required"),
432            (TaskState::AuthRequired, "auth-required"),
433            (TaskState::Completed, "completed"),
434            (TaskState::Failed, "failed"),
435            (TaskState::Canceled, "canceled"),
436            (TaskState::Rejected, "rejected"),
437        ];
438        for (state, expected) in &cases {
439            let json = serde_json::to_value(state).unwrap();
440            assert_eq!(
441                json.as_str().unwrap(),
442                *expected,
443                "serialization of {state:?}"
444            );
445            let deserialized: TaskState = serde_json::from_value(json).unwrap();
446            assert_eq!(deserialized, *state, "deserialization of {expected}");
447        }
448    }
449
450    #[test]
451    fn task_state_display() {
452        assert_eq!(TaskState::InputRequired.to_string(), "input-required");
453        assert_eq!(TaskState::AuthRequired.to_string(), "auth-required");
454        assert_eq!(TaskState::Completed.to_string(), "completed");
455    }
456
457    #[test]
458    fn agent_card_round_trip() {
459        let card = AgentCard {
460            name: "test-agent".to_string(),
461            description: "A test agent".to_string(),
462            version: "1.0.0".to_string(),
463            url: "https://example.com/a2a/test-agent".to_string(),
464            provider: Some(AgentProvider {
465                organization: "TestOrg".to_string(),
466                url: Some("https://testorg.com".to_string()),
467            }),
468            capabilities: AgentCapabilities {
469                streaming: Some(true),
470                push_notifications: Some(false),
471            },
472            security_schemes: None,
473            security_requirements: None,
474            default_input_modes: vec!["text/plain".to_string()],
475            default_output_modes: vec!["text/plain".to_string()],
476            skills: vec![AgentSkill {
477                id: "summarize".to_string(),
478                name: "Summarize".to_string(),
479                description: "Summarizes text".to_string(),
480                tags: vec!["nlp".to_string()],
481                examples: Some(vec!["Summarize this document".to_string()]),
482                input_modes: None,
483                output_modes: None,
484            }],
485            icon_url: None,
486            documentation_url: None,
487        };
488        let json = serde_json::to_string(&card).unwrap();
489        let deserialized: AgentCard = serde_json::from_str(&json).unwrap();
490        assert_eq!(deserialized, card);
491    }
492
493    #[test]
494    fn agent_card_camel_case_fields() {
495        let card = AgentCard {
496            name: "a".to_string(),
497            description: "d".to_string(),
498            version: "1".to_string(),
499            url: "http://x".to_string(),
500            provider: None,
501            capabilities: AgentCapabilities {
502                streaming: Some(true),
503                push_notifications: Some(true),
504            },
505            security_schemes: None,
506            security_requirements: None,
507            default_input_modes: vec![],
508            default_output_modes: vec![],
509            skills: vec![],
510            icon_url: None,
511            documentation_url: None,
512        };
513        let json = serde_json::to_value(&card).unwrap();
514        // Verify camelCase field names
515        assert!(json.get("defaultInputModes").is_some());
516        assert!(json.get("defaultOutputModes").is_some());
517        assert!(json["capabilities"].get("pushNotifications").is_some());
518    }
519
520    #[test]
521    fn task_push_notification_config_round_trip() {
522        let config = TaskPushNotificationConfig {
523            id: "cfg-1".to_string(),
524            task_id: "task-1".to_string(),
525            url: "https://webhook.example.com/notify".to_string(),
526            token: Some("secret-token".to_string()),
527            authentication: Some(AuthenticationInfo {
528                scheme: "Bearer".to_string(),
529                credentials: "my-jwt-token".to_string(),
530            }),
531        };
532        let json = serde_json::to_string(&config).unwrap();
533        let deserialized: TaskPushNotificationConfig = serde_json::from_str(&json).unwrap();
534        assert_eq!(deserialized, config);
535    }
536
537    #[test]
538    fn task_push_notification_config_minimal() {
539        let config = TaskPushNotificationConfig {
540            id: "cfg-2".to_string(),
541            task_id: "task-2".to_string(),
542            url: "https://example.com".to_string(),
543            token: None,
544            authentication: None,
545        };
546        let json = serde_json::to_value(&config).unwrap();
547        // Optional fields should be absent
548        assert!(json.get("token").is_none());
549        assert!(json.get("authentication").is_none());
550        let deserialized: TaskPushNotificationConfig = serde_json::from_value(json).unwrap();
551        assert_eq!(deserialized, config);
552    }
553
554    #[test]
555    fn artifact_round_trip() {
556        let artifact = Artifact {
557            artifact_id: "art-1".to_string(),
558            name: Some("output.txt".to_string()),
559            description: Some("Generated output".to_string()),
560            parts: vec![Part::Text {
561                text: "result data".to_string(),
562                metadata: None,
563            }],
564            metadata: None,
565            extensions: None,
566        };
567        let json = serde_json::to_string(&artifact).unwrap();
568        let deserialized: Artifact = serde_json::from_str(&json).unwrap();
569        assert_eq!(deserialized, artifact);
570    }
571
572    #[test]
573    fn security_scheme_api_key() {
574        let scheme = SecurityScheme::ApiKey {
575            name: "x-api-key".to_string(),
576            location: "header".to_string(),
577        };
578        let json = serde_json::to_value(&scheme).unwrap();
579        assert_eq!(json["type"], "apiKey");
580        assert_eq!(json["name"], "x-api-key");
581        assert_eq!(json["in"], "header");
582        let deserialized: SecurityScheme = serde_json::from_value(json).unwrap();
583        assert_eq!(deserialized, scheme);
584    }
585
586    #[test]
587    fn security_scheme_http_bearer() {
588        let scheme = SecurityScheme::Http {
589            scheme: "bearer".to_string(),
590            bearer_format: Some("JWT".to_string()),
591        };
592        let json = serde_json::to_value(&scheme).unwrap();
593        assert_eq!(json["type"], "http");
594        assert_eq!(json["scheme"], "bearer");
595        // bearer_format uses camelCase from the enum-level rename_all
596        let bf_key = if json.get("bearerFormat").is_some() {
597            "bearerFormat"
598        } else {
599            "bearer_format"
600        };
601        assert_eq!(json[bf_key], "JWT");
602        let deserialized: SecurityScheme = serde_json::from_value(json).unwrap();
603        assert_eq!(deserialized, scheme);
604    }
605}