1use serde::{Deserialize, Serialize};
10
11#[non_exhaustive]
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19pub enum TaskState {
20 #[serde(rename = "submitted")]
22 Submitted,
23 #[serde(rename = "working")]
25 Working,
26 #[serde(rename = "input-required")]
28 InputRequired,
29 #[serde(rename = "completed")]
31 Completed,
32 #[serde(rename = "failed")]
34 Failed,
35 #[serde(rename = "canceled")]
37 Canceled,
38 #[serde(rename = "rejected")]
40 Rejected,
41 #[serde(rename = "auth-required")]
43 AuthRequired,
44 #[serde(rename = "unknown")]
46 Unknown,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
55#[serde(rename_all = "camelCase")]
56pub struct Task {
57 pub id: String,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub context_id: Option<String>,
62 pub status: TaskStatus,
64 #[serde(default, skip_serializing_if = "Vec::is_empty")]
66 pub artifacts: Vec<Artifact>,
67 #[serde(default, skip_serializing_if = "Vec::is_empty")]
69 pub history: Vec<Message>,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub metadata: Option<serde_json::Value>,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77#[serde(rename_all = "camelCase")]
78pub struct TaskStatus {
79 pub state: TaskState,
81 pub timestamp: String,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub message: Option<Message>,
86}
87
88#[non_exhaustive]
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
91#[serde(rename_all = "lowercase")]
92pub enum Role {
93 User,
95 Agent,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(rename_all = "camelCase")]
115pub struct Message {
116 pub role: Role,
118 pub parts: Vec<Part>,
120 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub message_id: Option<String>,
123 #[serde(default, skip_serializing_if = "Option::is_none")]
125 pub task_id: Option<String>,
126 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub context_id: Option<String>,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub metadata: Option<serde_json::Value>,
132}
133
134#[non_exhaustive]
149#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
150#[serde(tag = "kind", rename_all = "lowercase")]
151pub enum Part {
152 Text {
154 text: String,
155 #[serde(default, skip_serializing_if = "Option::is_none")]
156 metadata: Option<serde_json::Value>,
157 },
158 File {
160 file: FileContent,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
162 metadata: Option<serde_json::Value>,
163 },
164 Data {
166 data: serde_json::Value,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
168 metadata: Option<serde_json::Value>,
169 },
170}
171
172#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
177#[serde(rename_all = "camelCase")]
178pub struct FileContent {
179 #[serde(default, skip_serializing_if = "Option::is_none")]
181 pub name: Option<String>,
182 #[serde(default, skip_serializing_if = "Option::is_none")]
184 pub media_type: Option<String>,
185 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub file_with_bytes: Option<String>,
188 #[serde(default, skip_serializing_if = "Option::is_none")]
190 pub file_with_uri: Option<String>,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
198#[serde(rename_all = "camelCase")]
199pub struct Artifact {
200 pub artifact_id: String,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub name: Option<String>,
205 pub parts: Vec<Part>,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
209 pub metadata: Option<serde_json::Value>,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
234#[serde(rename_all = "camelCase")]
235pub struct AgentCard {
236 pub name: String,
238 pub description: String,
240 pub url: String,
242 pub version: String,
244 pub protocol_version: String,
246 #[serde(default, skip_serializing_if = "Option::is_none")]
248 pub provider: Option<AgentProvider>,
249 pub capabilities: AgentCapabilities,
251 #[serde(default, skip_serializing_if = "Vec::is_empty")]
253 pub default_input_modes: Vec<String>,
254 #[serde(default, skip_serializing_if = "Vec::is_empty")]
256 pub default_output_modes: Vec<String>,
257 #[serde(default, skip_serializing_if = "Vec::is_empty")]
259 pub skills: Vec<AgentSkill>,
260 #[serde(default, skip_serializing_if = "Vec::is_empty")]
266 pub signatures: Vec<AgentCardSignature>,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
275#[serde(rename_all = "camelCase")]
276pub struct AgentCardSignature {
277 pub protected: String,
279 pub signature: String,
281 #[serde(default, skip_serializing_if = "Option::is_none")]
283 pub header: Option<serde_json::Value>,
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize)]
288#[serde(rename_all = "camelCase")]
289pub struct AgentProvider {
290 pub organization: String,
292 #[serde(default, skip_serializing_if = "Option::is_none")]
294 pub url: Option<String>,
295}
296
297#[derive(Debug, Clone, Default, Serialize, Deserialize)]
307#[serde(rename_all = "camelCase")]
308#[allow(clippy::struct_excessive_bools)] pub struct AgentCapabilities {
310 #[serde(default)]
312 pub streaming: bool,
313 #[serde(default)]
315 pub push_notifications: bool,
316 #[serde(default)]
318 pub state_transition_history: bool,
319 #[serde(default)]
323 pub images: bool,
324 #[serde(default)]
328 pub audio: bool,
329 #[serde(default)]
333 pub files: bool,
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize)]
341#[serde(rename_all = "camelCase")]
342pub struct AgentSkill {
343 pub id: String,
345 pub name: String,
347 pub description: String,
349 #[serde(default, skip_serializing_if = "Vec::is_empty")]
351 pub tags: Vec<String>,
352 #[serde(default, skip_serializing_if = "Vec::is_empty")]
354 pub examples: Vec<String>,
355 #[serde(default, skip_serializing_if = "Vec::is_empty")]
357 pub input_modes: Vec<String>,
358 #[serde(default, skip_serializing_if = "Vec::is_empty")]
360 pub output_modes: Vec<String>,
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize)]
368#[serde(rename_all = "camelCase")]
369pub struct TaskStatusUpdateEvent {
370 #[serde(default = "kind_status_update")]
372 pub kind: String,
373 pub task_id: String,
375 #[serde(default, skip_serializing_if = "Option::is_none")]
377 pub context_id: Option<String>,
378 pub status: TaskStatus,
380 #[serde(rename = "final", default)]
382 pub is_final: bool,
383}
384
385fn kind_status_update() -> String {
386 "status-update".into()
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize)]
394#[serde(rename_all = "camelCase")]
395pub struct TaskArtifactUpdateEvent {
396 #[serde(default = "kind_artifact_update")]
398 pub kind: String,
399 pub task_id: String,
401 #[serde(default, skip_serializing_if = "Option::is_none")]
403 pub context_id: Option<String>,
404 pub artifact: Artifact,
406 #[serde(rename = "final", default)]
408 pub is_final: bool,
409}
410
411fn kind_artifact_update() -> String {
412 "artifact-update".into()
413}
414
415impl Part {
416 #[must_use]
427 pub fn text(s: impl Into<String>) -> Self {
428 Self::Text {
429 text: s.into(),
430 metadata: None,
431 }
432 }
433}
434
435impl Message {
436 #[must_use]
450 pub fn user_text(s: impl Into<String>) -> Self {
451 Self {
452 role: Role::User,
453 parts: vec![Part::text(s)],
454 message_id: None,
455 task_id: None,
456 context_id: None,
457 metadata: None,
458 }
459 }
460
461 #[must_use]
474 pub fn text_content(&self) -> Option<&str> {
475 self.parts.iter().find_map(|p| match p {
476 Part::Text { text, .. } => Some(text.as_str()),
477 _ => None,
478 })
479 }
480
481 #[must_use]
487 pub fn all_text_content(&self) -> String {
488 let parts: Vec<&str> = self
489 .parts
490 .iter()
491 .filter_map(|p| match p {
492 Part::Text { text, .. } => Some(text.as_str()),
493 _ => None,
494 })
495 .collect();
496 parts.join("\n\n")
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503
504 #[test]
505 fn task_state_serde() {
506 let states = [
507 (TaskState::Submitted, "\"submitted\""),
508 (TaskState::Working, "\"working\""),
509 (TaskState::InputRequired, "\"input-required\""),
510 (TaskState::Completed, "\"completed\""),
511 (TaskState::Failed, "\"failed\""),
512 (TaskState::Canceled, "\"canceled\""),
513 (TaskState::Rejected, "\"rejected\""),
514 (TaskState::AuthRequired, "\"auth-required\""),
515 (TaskState::Unknown, "\"unknown\""),
516 ];
517 for (state, expected) in states {
518 let json = serde_json::to_string(&state).unwrap();
519 assert_eq!(json, expected, "serialization mismatch for {state:?}");
520 let back: TaskState = serde_json::from_str(&json).unwrap();
521 assert_eq!(back, state);
522 }
523 }
524
525 #[test]
526 fn role_serde_lowercase() {
527 assert_eq!(serde_json::to_string(&Role::User).unwrap(), "\"user\"");
528 assert_eq!(serde_json::to_string(&Role::Agent).unwrap(), "\"agent\"");
529 }
530
531 #[test]
532 fn part_text_constructor() {
533 let part = Part::text("hello");
534 assert_eq!(
535 part,
536 Part::Text {
537 text: "hello".into(),
538 metadata: None
539 }
540 );
541 }
542
543 #[test]
544 fn part_kind_serde() {
545 let text_part = Part::text("hello");
546 let json = serde_json::to_string(&text_part).unwrap();
547 assert!(json.contains("\"kind\":\"text\""));
548 assert!(json.contains("\"text\":\"hello\""));
549 let back: Part = serde_json::from_str(&json).unwrap();
550 assert_eq!(back, text_part);
551
552 let file_part = Part::File {
553 file: FileContent {
554 name: Some("doc.pdf".into()),
555 media_type: None,
556 file_with_bytes: None,
557 file_with_uri: Some("https://example.com/doc.pdf".into()),
558 },
559 metadata: None,
560 };
561 let json = serde_json::to_string(&file_part).unwrap();
562 assert!(json.contains("\"kind\":\"file\""));
563 let back: Part = serde_json::from_str(&json).unwrap();
564 assert_eq!(back, file_part);
565
566 let data_part = Part::Data {
567 data: serde_json::json!({"key": "value"}),
568 metadata: None,
569 };
570 let json = serde_json::to_string(&data_part).unwrap();
571 assert!(json.contains("\"kind\":\"data\""));
572 let back: Part = serde_json::from_str(&json).unwrap();
573 assert_eq!(back, data_part);
574 }
575
576 #[test]
577 fn message_user_text_constructor() {
578 let msg = Message::user_text("test input");
579 assert_eq!(msg.role, Role::User);
580 assert_eq!(msg.text_content(), Some("test input"));
581 }
582
583 #[test]
584 fn message_serde_round_trip() {
585 let msg = Message::user_text("hello agent");
586 let json = serde_json::to_string(&msg).unwrap();
587 let back: Message = serde_json::from_str(&json).unwrap();
588 assert_eq!(back.role, Role::User);
589 assert_eq!(back.text_content(), Some("hello agent"));
590 }
591
592 #[test]
593 fn task_serde_round_trip() {
594 let task = Task {
595 id: "task-1".into(),
596 context_id: None,
597 status: TaskStatus {
598 state: TaskState::Working,
599 timestamp: "2025-01-01T00:00:00Z".into(),
600 message: None,
601 },
602 artifacts: vec![],
603 history: vec![Message::user_text("do something")],
604 metadata: None,
605 };
606 let json = serde_json::to_string(&task).unwrap();
607 assert!(json.contains("\"contextId\"").not());
608 let back: Task = serde_json::from_str(&json).unwrap();
609 assert_eq!(back.id, "task-1");
610 assert_eq!(back.status.state, TaskState::Working);
611 assert_eq!(back.history.len(), 1);
612 }
613
614 #[test]
615 fn task_skips_empty_vecs_and_none() {
616 let task = Task {
617 id: "t".into(),
618 context_id: None,
619 status: TaskStatus {
620 state: TaskState::Submitted,
621 timestamp: "ts".into(),
622 message: None,
623 },
624 artifacts: vec![],
625 history: vec![],
626 metadata: None,
627 };
628 let json = serde_json::to_string(&task).unwrap();
629 assert!(!json.contains("artifacts"));
630 assert!(!json.contains("history"));
631 assert!(!json.contains("metadata"));
632 assert!(!json.contains("contextId"));
633 }
634
635 #[test]
636 fn artifact_serde_round_trip() {
637 let artifact = Artifact {
638 artifact_id: "art-1".into(),
639 name: Some("result.txt".into()),
640 parts: vec![Part::text("file content")],
641 metadata: None,
642 };
643 let json = serde_json::to_string(&artifact).unwrap();
644 assert!(json.contains("\"artifactId\""));
645 let back: Artifact = serde_json::from_str(&json).unwrap();
646 assert_eq!(back.artifact_id, "art-1");
647 }
648
649 #[test]
650 fn agent_card_serde_round_trip() {
651 let card = AgentCard {
652 name: "test-agent".into(),
653 description: "A test agent".into(),
654 url: "http://localhost:8080".into(),
655 version: "0.1.0".into(),
656 protocol_version: "0.2.1".into(),
657 provider: Some(AgentProvider {
658 organization: "TestOrg".into(),
659 url: Some("https://test.org".into()),
660 }),
661 capabilities: AgentCapabilities {
662 streaming: true,
663 push_notifications: false,
664 state_transition_history: false,
665 images: false,
666 audio: false,
667 files: false,
668 },
669 default_input_modes: vec!["text".into()],
670 default_output_modes: vec!["text".into()],
671 skills: vec![AgentSkill {
672 id: "skill-1".into(),
673 name: "Test Skill".into(),
674 description: "Does testing".into(),
675 tags: vec!["test".into()],
676 examples: vec![],
677 input_modes: vec![],
678 output_modes: vec![],
679 }],
680 signatures: vec![],
681 };
682 let json = serde_json::to_string_pretty(&card).unwrap();
683 let back: AgentCard = serde_json::from_str(&json).unwrap();
684 assert_eq!(back.name, "test-agent");
685 assert!(back.capabilities.streaming);
686 assert_eq!(back.skills.len(), 1);
687 }
688
689 #[test]
690 fn agent_card_signatures_default_empty_and_skipped() {
691 let card = minimal_card();
692 let json = serde_json::to_string(&card).unwrap();
693 assert!(!json.contains("signatures"));
694 let back: AgentCard = serde_json::from_str(&json).unwrap();
695 assert!(back.signatures.is_empty());
696 }
697
698 #[test]
699 fn agent_card_deserializes_legacy_card_without_signatures_key() {
700 let json = r#"{"name":"old","description":"","url":"http://x","version":"1","protocolVersion":"0.2.1","capabilities":{"streaming":false}}"#;
702 let card: AgentCard = serde_json::from_str(json).unwrap();
703 assert!(card.signatures.is_empty());
704 }
705
706 #[test]
707 fn agent_card_signature_round_trips() {
708 let sig = AgentCardSignature {
709 protected: "eyJhbGciOiJFUzI1NiJ9".into(),
710 signature: "c2lnbmF0dXJlLWJ5dGVz".into(),
711 header: Some(serde_json::json!({"kid": "key-1"})),
712 };
713 let json = serde_json::to_string(&sig).unwrap();
714 let back: AgentCardSignature = serde_json::from_str(&json).unwrap();
715 assert_eq!(back.protected, sig.protected);
716 assert_eq!(back.signature, sig.signature);
717 assert_eq!(back.header, sig.header);
718 }
719
720 fn minimal_card() -> AgentCard {
721 AgentCard {
722 name: "test-agent".into(),
723 description: "A test agent".into(),
724 url: "http://localhost:8080".into(),
725 version: "0.1.0".into(),
726 protocol_version: "0.2.1".into(),
727 provider: None,
728 capabilities: AgentCapabilities::default(),
729 default_input_modes: vec![],
730 default_output_modes: vec![],
731 skills: vec![],
732 signatures: vec![],
733 }
734 }
735
736 #[test]
737 fn task_status_update_event_serde() {
738 let event = TaskStatusUpdateEvent {
739 kind: "status-update".into(),
740 task_id: "t-1".into(),
741 context_id: None,
742 status: TaskStatus {
743 state: TaskState::Completed,
744 timestamp: "ts".into(),
745 message: None,
746 },
747 is_final: true,
748 };
749 let json = serde_json::to_string(&event).unwrap();
750 assert!(json.contains("\"final\":true"));
751 assert!(!json.contains("isFinal"));
752 assert!(json.contains("\"kind\":\"status-update\""));
753 let back: TaskStatusUpdateEvent = serde_json::from_str(&json).unwrap();
754 assert!(back.is_final);
755 assert_eq!(back.kind, "status-update");
756 }
757
758 #[test]
759 fn task_artifact_update_event_serde() {
760 let event = TaskArtifactUpdateEvent {
761 kind: "artifact-update".into(),
762 task_id: "t-1".into(),
763 context_id: None,
764 artifact: Artifact {
765 artifact_id: "a-1".into(),
766 name: None,
767 parts: vec![Part::text("data")],
768 metadata: None,
769 },
770 is_final: false,
771 };
772 let json = serde_json::to_string(&event).unwrap();
773 assert!(json.contains("\"final\":false"));
774 assert!(json.contains("\"kind\":\"artifact-update\""));
775 let back: TaskArtifactUpdateEvent = serde_json::from_str(&json).unwrap();
776 assert!(!back.is_final);
777 assert_eq!(back.kind, "artifact-update");
778 }
779
780 #[test]
781 fn file_content_serde() {
782 let fc = FileContent {
783 name: Some("doc.pdf".into()),
784 media_type: Some("application/pdf".into()),
785 file_with_bytes: Some("base64data==".into()),
786 file_with_uri: None,
787 };
788 let json = serde_json::to_string(&fc).unwrap();
789 assert!(json.contains("\"mediaType\""));
790 assert!(json.contains("\"fileWithBytes\""));
791 assert!(!json.contains("fileWithUri"));
792 let back: FileContent = serde_json::from_str(&json).unwrap();
793 assert_eq!(back.name.as_deref(), Some("doc.pdf"));
794 }
795
796 #[test]
797 fn all_text_content_single_part() {
798 let msg = Message::user_text("hello world");
799 assert_eq!(msg.all_text_content(), "hello world");
800 }
801
802 #[test]
803 fn all_text_content_multiple_parts_joined() {
804 let msg = Message {
805 role: Role::User,
806 parts: vec![
807 Part::text("first"),
808 Part::text("second"),
809 Part::text("third"),
810 ],
811 message_id: None,
812 task_id: None,
813 context_id: None,
814 metadata: None,
815 };
816 assert_eq!(msg.all_text_content(), "first\n\nsecond\n\nthird");
817 }
818
819 #[test]
820 fn all_text_content_no_text_parts_returns_empty() {
821 let msg = Message {
822 role: Role::User,
823 parts: vec![],
824 message_id: None,
825 task_id: None,
826 context_id: None,
827 metadata: None,
828 };
829 assert_eq!(msg.all_text_content(), "");
830 }
831
832 #[test]
833 fn all_text_content_skips_non_text_parts() {
834 let msg = Message {
835 role: Role::User,
836 parts: vec![
837 Part::text("text-only"),
838 Part::Data {
839 data: serde_json::json!({"key": "val"}),
840 metadata: None,
841 },
842 ],
843 message_id: None,
844 task_id: None,
845 context_id: None,
846 metadata: None,
847 };
848 assert_eq!(msg.all_text_content(), "text-only");
849 }
850
851 #[test]
852 fn agent_capabilities_default_has_no_modalities() {
853 let caps = AgentCapabilities::default();
854 assert!(!caps.images);
855 assert!(!caps.audio);
856 assert!(!caps.files);
857 }
858
859 #[test]
860 fn agent_capabilities_modality_fields_serialize() {
861 let caps = AgentCapabilities {
862 streaming: false,
863 push_notifications: false,
864 state_transition_history: false,
865 images: false,
866 audio: false,
867 files: false,
868 };
869 let json = serde_json::to_string(&caps).unwrap();
870 assert!(json.contains("\"images\":false"));
871 assert!(json.contains("\"audio\":false"));
872 assert!(json.contains("\"files\":false"));
873 }
874
875 #[test]
876 fn deserialize_legacy_capabilities_uses_modality_defaults() {
877 let json = r#"{"streaming": true}"#;
879 let caps: AgentCapabilities = serde_json::from_str(json).unwrap();
880 assert!(caps.streaming);
881 assert!(!caps.images);
882 assert!(!caps.audio);
883 assert!(!caps.files);
884 }
885
886 trait Not {
887 fn not(&self) -> bool;
888 }
889 impl Not for bool {
890 fn not(&self) -> bool {
891 !*self
892 }
893 }
894}