1use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18pub enum TaskState {
19 #[serde(rename = "submitted")]
21 Submitted,
22 #[serde(rename = "working")]
24 Working,
25 #[serde(rename = "input-required")]
27 InputRequired,
28 #[serde(rename = "completed")]
30 Completed,
31 #[serde(rename = "failed")]
33 Failed,
34 #[serde(rename = "canceled")]
36 Canceled,
37 #[serde(rename = "rejected")]
39 Rejected,
40 #[serde(rename = "auth-required")]
42 AuthRequired,
43 #[serde(rename = "unknown")]
45 Unknown,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub struct Task {
56 pub id: String,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub context_id: Option<String>,
61 pub status: TaskStatus,
63 #[serde(default, skip_serializing_if = "Vec::is_empty")]
65 pub artifacts: Vec<Artifact>,
66 #[serde(default, skip_serializing_if = "Vec::is_empty")]
68 pub history: Vec<Message>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub metadata: Option<serde_json::Value>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76#[serde(rename_all = "camelCase")]
77pub struct TaskStatus {
78 pub state: TaskState,
80 pub timestamp: String,
82 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub message: Option<Message>,
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(rename_all = "lowercase")]
90pub enum Role {
91 User,
93 Agent,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
112#[serde(rename_all = "camelCase")]
113pub struct Message {
114 pub role: Role,
116 pub parts: Vec<Part>,
118 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub message_id: Option<String>,
121 #[serde(default, skip_serializing_if = "Option::is_none")]
123 pub task_id: Option<String>,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub context_id: Option<String>,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub metadata: Option<serde_json::Value>,
130}
131
132#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147#[serde(tag = "kind", rename_all = "lowercase")]
148pub enum Part {
149 Text {
151 text: String,
152 #[serde(default, skip_serializing_if = "Option::is_none")]
153 metadata: Option<serde_json::Value>,
154 },
155 File {
157 file: FileContent,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
159 metadata: Option<serde_json::Value>,
160 },
161 Data {
163 data: serde_json::Value,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
165 metadata: Option<serde_json::Value>,
166 },
167}
168
169#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
174#[serde(rename_all = "camelCase")]
175pub struct FileContent {
176 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub name: Option<String>,
179 #[serde(default, skip_serializing_if = "Option::is_none")]
181 pub media_type: Option<String>,
182 #[serde(default, skip_serializing_if = "Option::is_none")]
184 pub file_with_bytes: Option<String>,
185 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub file_with_uri: Option<String>,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
195#[serde(rename_all = "camelCase")]
196pub struct Artifact {
197 pub artifact_id: String,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub name: Option<String>,
202 pub parts: Vec<Part>,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub metadata: Option<serde_json::Value>,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
231#[serde(rename_all = "camelCase")]
232pub struct AgentCard {
233 pub name: String,
235 pub description: String,
237 pub url: String,
239 pub version: String,
241 pub protocol_version: String,
243 #[serde(default, skip_serializing_if = "Option::is_none")]
245 pub provider: Option<AgentProvider>,
246 pub capabilities: AgentCapabilities,
248 #[serde(default, skip_serializing_if = "Vec::is_empty")]
250 pub default_input_modes: Vec<String>,
251 #[serde(default, skip_serializing_if = "Vec::is_empty")]
253 pub default_output_modes: Vec<String>,
254 #[serde(default, skip_serializing_if = "Vec::is_empty")]
256 pub skills: Vec<AgentSkill>,
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize)]
261#[serde(rename_all = "camelCase")]
262pub struct AgentProvider {
263 pub organization: String,
265 #[serde(default, skip_serializing_if = "Option::is_none")]
267 pub url: Option<String>,
268}
269
270#[derive(Debug, Clone, Default, Serialize, Deserialize)]
280#[serde(rename_all = "camelCase")]
281#[allow(clippy::struct_excessive_bools)] pub struct AgentCapabilities {
283 #[serde(default)]
285 pub streaming: bool,
286 #[serde(default)]
288 pub push_notifications: bool,
289 #[serde(default)]
291 pub state_transition_history: bool,
292 #[serde(default)]
296 pub images: bool,
297 #[serde(default)]
301 pub audio: bool,
302 #[serde(default)]
306 pub files: bool,
307}
308
309#[derive(Debug, Clone, Serialize, Deserialize)]
314#[serde(rename_all = "camelCase")]
315pub struct AgentSkill {
316 pub id: String,
318 pub name: String,
320 pub description: String,
322 #[serde(default, skip_serializing_if = "Vec::is_empty")]
324 pub tags: Vec<String>,
325 #[serde(default, skip_serializing_if = "Vec::is_empty")]
327 pub examples: Vec<String>,
328 #[serde(default, skip_serializing_if = "Vec::is_empty")]
330 pub input_modes: Vec<String>,
331 #[serde(default, skip_serializing_if = "Vec::is_empty")]
333 pub output_modes: Vec<String>,
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize)]
341#[serde(rename_all = "camelCase")]
342pub struct TaskStatusUpdateEvent {
343 #[serde(default = "kind_status_update")]
345 pub kind: String,
346 pub task_id: String,
348 #[serde(default, skip_serializing_if = "Option::is_none")]
350 pub context_id: Option<String>,
351 pub status: TaskStatus,
353 #[serde(rename = "final", default)]
355 pub is_final: bool,
356}
357
358fn kind_status_update() -> String {
359 "status-update".into()
360}
361
362#[derive(Debug, Clone, Serialize, Deserialize)]
367#[serde(rename_all = "camelCase")]
368pub struct TaskArtifactUpdateEvent {
369 #[serde(default = "kind_artifact_update")]
371 pub kind: String,
372 pub task_id: String,
374 #[serde(default, skip_serializing_if = "Option::is_none")]
376 pub context_id: Option<String>,
377 pub artifact: Artifact,
379 #[serde(rename = "final", default)]
381 pub is_final: bool,
382}
383
384fn kind_artifact_update() -> String {
385 "artifact-update".into()
386}
387
388impl Part {
389 #[must_use]
400 pub fn text(s: impl Into<String>) -> Self {
401 Self::Text {
402 text: s.into(),
403 metadata: None,
404 }
405 }
406}
407
408impl Message {
409 #[must_use]
423 pub fn user_text(s: impl Into<String>) -> Self {
424 Self {
425 role: Role::User,
426 parts: vec![Part::text(s)],
427 message_id: None,
428 task_id: None,
429 context_id: None,
430 metadata: None,
431 }
432 }
433
434 #[must_use]
447 pub fn text_content(&self) -> Option<&str> {
448 self.parts.iter().find_map(|p| match p {
449 Part::Text { text, .. } => Some(text.as_str()),
450 _ => None,
451 })
452 }
453
454 #[must_use]
460 pub fn all_text_content(&self) -> String {
461 let parts: Vec<&str> = self
462 .parts
463 .iter()
464 .filter_map(|p| match p {
465 Part::Text { text, .. } => Some(text.as_str()),
466 _ => None,
467 })
468 .collect();
469 parts.join("\n\n")
470 }
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476
477 #[test]
478 fn task_state_serde() {
479 let states = [
480 (TaskState::Submitted, "\"submitted\""),
481 (TaskState::Working, "\"working\""),
482 (TaskState::InputRequired, "\"input-required\""),
483 (TaskState::Completed, "\"completed\""),
484 (TaskState::Failed, "\"failed\""),
485 (TaskState::Canceled, "\"canceled\""),
486 (TaskState::Rejected, "\"rejected\""),
487 (TaskState::AuthRequired, "\"auth-required\""),
488 (TaskState::Unknown, "\"unknown\""),
489 ];
490 for (state, expected) in states {
491 let json = serde_json::to_string(&state).unwrap();
492 assert_eq!(json, expected, "serialization mismatch for {state:?}");
493 let back: TaskState = serde_json::from_str(&json).unwrap();
494 assert_eq!(back, state);
495 }
496 }
497
498 #[test]
499 fn role_serde_lowercase() {
500 assert_eq!(serde_json::to_string(&Role::User).unwrap(), "\"user\"");
501 assert_eq!(serde_json::to_string(&Role::Agent).unwrap(), "\"agent\"");
502 }
503
504 #[test]
505 fn part_text_constructor() {
506 let part = Part::text("hello");
507 assert_eq!(
508 part,
509 Part::Text {
510 text: "hello".into(),
511 metadata: None
512 }
513 );
514 }
515
516 #[test]
517 fn part_kind_serde() {
518 let text_part = Part::text("hello");
519 let json = serde_json::to_string(&text_part).unwrap();
520 assert!(json.contains("\"kind\":\"text\""));
521 assert!(json.contains("\"text\":\"hello\""));
522 let back: Part = serde_json::from_str(&json).unwrap();
523 assert_eq!(back, text_part);
524
525 let file_part = Part::File {
526 file: FileContent {
527 name: Some("doc.pdf".into()),
528 media_type: None,
529 file_with_bytes: None,
530 file_with_uri: Some("https://example.com/doc.pdf".into()),
531 },
532 metadata: None,
533 };
534 let json = serde_json::to_string(&file_part).unwrap();
535 assert!(json.contains("\"kind\":\"file\""));
536 let back: Part = serde_json::from_str(&json).unwrap();
537 assert_eq!(back, file_part);
538
539 let data_part = Part::Data {
540 data: serde_json::json!({"key": "value"}),
541 metadata: None,
542 };
543 let json = serde_json::to_string(&data_part).unwrap();
544 assert!(json.contains("\"kind\":\"data\""));
545 let back: Part = serde_json::from_str(&json).unwrap();
546 assert_eq!(back, data_part);
547 }
548
549 #[test]
550 fn message_user_text_constructor() {
551 let msg = Message::user_text("test input");
552 assert_eq!(msg.role, Role::User);
553 assert_eq!(msg.text_content(), Some("test input"));
554 }
555
556 #[test]
557 fn message_serde_round_trip() {
558 let msg = Message::user_text("hello agent");
559 let json = serde_json::to_string(&msg).unwrap();
560 let back: Message = serde_json::from_str(&json).unwrap();
561 assert_eq!(back.role, Role::User);
562 assert_eq!(back.text_content(), Some("hello agent"));
563 }
564
565 #[test]
566 fn task_serde_round_trip() {
567 let task = Task {
568 id: "task-1".into(),
569 context_id: None,
570 status: TaskStatus {
571 state: TaskState::Working,
572 timestamp: "2025-01-01T00:00:00Z".into(),
573 message: None,
574 },
575 artifacts: vec![],
576 history: vec![Message::user_text("do something")],
577 metadata: None,
578 };
579 let json = serde_json::to_string(&task).unwrap();
580 assert!(json.contains("\"contextId\"").not());
581 let back: Task = serde_json::from_str(&json).unwrap();
582 assert_eq!(back.id, "task-1");
583 assert_eq!(back.status.state, TaskState::Working);
584 assert_eq!(back.history.len(), 1);
585 }
586
587 #[test]
588 fn task_skips_empty_vecs_and_none() {
589 let task = Task {
590 id: "t".into(),
591 context_id: None,
592 status: TaskStatus {
593 state: TaskState::Submitted,
594 timestamp: "ts".into(),
595 message: None,
596 },
597 artifacts: vec![],
598 history: vec![],
599 metadata: None,
600 };
601 let json = serde_json::to_string(&task).unwrap();
602 assert!(!json.contains("artifacts"));
603 assert!(!json.contains("history"));
604 assert!(!json.contains("metadata"));
605 assert!(!json.contains("contextId"));
606 }
607
608 #[test]
609 fn artifact_serde_round_trip() {
610 let artifact = Artifact {
611 artifact_id: "art-1".into(),
612 name: Some("result.txt".into()),
613 parts: vec![Part::text("file content")],
614 metadata: None,
615 };
616 let json = serde_json::to_string(&artifact).unwrap();
617 assert!(json.contains("\"artifactId\""));
618 let back: Artifact = serde_json::from_str(&json).unwrap();
619 assert_eq!(back.artifact_id, "art-1");
620 }
621
622 #[test]
623 fn agent_card_serde_round_trip() {
624 let card = AgentCard {
625 name: "test-agent".into(),
626 description: "A test agent".into(),
627 url: "http://localhost:8080".into(),
628 version: "0.1.0".into(),
629 protocol_version: "0.2.1".into(),
630 provider: Some(AgentProvider {
631 organization: "TestOrg".into(),
632 url: Some("https://test.org".into()),
633 }),
634 capabilities: AgentCapabilities {
635 streaming: true,
636 push_notifications: false,
637 state_transition_history: false,
638 images: false,
639 audio: false,
640 files: false,
641 },
642 default_input_modes: vec!["text".into()],
643 default_output_modes: vec!["text".into()],
644 skills: vec![AgentSkill {
645 id: "skill-1".into(),
646 name: "Test Skill".into(),
647 description: "Does testing".into(),
648 tags: vec!["test".into()],
649 examples: vec![],
650 input_modes: vec![],
651 output_modes: vec![],
652 }],
653 };
654 let json = serde_json::to_string_pretty(&card).unwrap();
655 let back: AgentCard = serde_json::from_str(&json).unwrap();
656 assert_eq!(back.name, "test-agent");
657 assert!(back.capabilities.streaming);
658 assert_eq!(back.skills.len(), 1);
659 }
660
661 #[test]
662 fn task_status_update_event_serde() {
663 let event = TaskStatusUpdateEvent {
664 kind: "status-update".into(),
665 task_id: "t-1".into(),
666 context_id: None,
667 status: TaskStatus {
668 state: TaskState::Completed,
669 timestamp: "ts".into(),
670 message: None,
671 },
672 is_final: true,
673 };
674 let json = serde_json::to_string(&event).unwrap();
675 assert!(json.contains("\"final\":true"));
676 assert!(!json.contains("isFinal"));
677 assert!(json.contains("\"kind\":\"status-update\""));
678 let back: TaskStatusUpdateEvent = serde_json::from_str(&json).unwrap();
679 assert!(back.is_final);
680 assert_eq!(back.kind, "status-update");
681 }
682
683 #[test]
684 fn task_artifact_update_event_serde() {
685 let event = TaskArtifactUpdateEvent {
686 kind: "artifact-update".into(),
687 task_id: "t-1".into(),
688 context_id: None,
689 artifact: Artifact {
690 artifact_id: "a-1".into(),
691 name: None,
692 parts: vec![Part::text("data")],
693 metadata: None,
694 },
695 is_final: false,
696 };
697 let json = serde_json::to_string(&event).unwrap();
698 assert!(json.contains("\"final\":false"));
699 assert!(json.contains("\"kind\":\"artifact-update\""));
700 let back: TaskArtifactUpdateEvent = serde_json::from_str(&json).unwrap();
701 assert!(!back.is_final);
702 assert_eq!(back.kind, "artifact-update");
703 }
704
705 #[test]
706 fn file_content_serde() {
707 let fc = FileContent {
708 name: Some("doc.pdf".into()),
709 media_type: Some("application/pdf".into()),
710 file_with_bytes: Some("base64data==".into()),
711 file_with_uri: None,
712 };
713 let json = serde_json::to_string(&fc).unwrap();
714 assert!(json.contains("\"mediaType\""));
715 assert!(json.contains("\"fileWithBytes\""));
716 assert!(!json.contains("fileWithUri"));
717 let back: FileContent = serde_json::from_str(&json).unwrap();
718 assert_eq!(back.name.as_deref(), Some("doc.pdf"));
719 }
720
721 #[test]
722 fn all_text_content_single_part() {
723 let msg = Message::user_text("hello world");
724 assert_eq!(msg.all_text_content(), "hello world");
725 }
726
727 #[test]
728 fn all_text_content_multiple_parts_joined() {
729 let msg = Message {
730 role: Role::User,
731 parts: vec![
732 Part::text("first"),
733 Part::text("second"),
734 Part::text("third"),
735 ],
736 message_id: None,
737 task_id: None,
738 context_id: None,
739 metadata: None,
740 };
741 assert_eq!(msg.all_text_content(), "first\n\nsecond\n\nthird");
742 }
743
744 #[test]
745 fn all_text_content_no_text_parts_returns_empty() {
746 let msg = Message {
747 role: Role::User,
748 parts: vec![],
749 message_id: None,
750 task_id: None,
751 context_id: None,
752 metadata: None,
753 };
754 assert_eq!(msg.all_text_content(), "");
755 }
756
757 #[test]
758 fn all_text_content_skips_non_text_parts() {
759 let msg = Message {
760 role: Role::User,
761 parts: vec![
762 Part::text("text-only"),
763 Part::Data {
764 data: serde_json::json!({"key": "val"}),
765 metadata: None,
766 },
767 ],
768 message_id: None,
769 task_id: None,
770 context_id: None,
771 metadata: None,
772 };
773 assert_eq!(msg.all_text_content(), "text-only");
774 }
775
776 #[test]
777 fn agent_capabilities_default_has_no_modalities() {
778 let caps = AgentCapabilities::default();
779 assert!(!caps.images);
780 assert!(!caps.audio);
781 assert!(!caps.files);
782 }
783
784 #[test]
785 fn agent_capabilities_modality_fields_serialize() {
786 let caps = AgentCapabilities {
787 streaming: false,
788 push_notifications: false,
789 state_transition_history: false,
790 images: false,
791 audio: false,
792 files: false,
793 };
794 let json = serde_json::to_string(&caps).unwrap();
795 assert!(json.contains("\"images\":false"));
796 assert!(json.contains("\"audio\":false"));
797 assert!(json.contains("\"files\":false"));
798 }
799
800 #[test]
801 fn deserialize_legacy_capabilities_uses_modality_defaults() {
802 let json = r#"{"streaming": true}"#;
804 let caps: AgentCapabilities = serde_json::from_str(json).unwrap();
805 assert!(caps.streaming);
806 assert!(!caps.images);
807 assert!(!caps.audio);
808 assert!(!caps.files);
809 }
810
811 trait Not {
812 fn not(&self) -> bool;
813 }
814 impl Not for bool {
815 fn not(&self) -> bool {
816 !*self
817 }
818 }
819}