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}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
264#[serde(rename_all = "camelCase")]
265pub struct AgentProvider {
266 pub organization: String,
268 #[serde(default, skip_serializing_if = "Option::is_none")]
270 pub url: Option<String>,
271}
272
273#[derive(Debug, Clone, Default, Serialize, Deserialize)]
283#[serde(rename_all = "camelCase")]
284#[allow(clippy::struct_excessive_bools)] pub struct AgentCapabilities {
286 #[serde(default)]
288 pub streaming: bool,
289 #[serde(default)]
291 pub push_notifications: bool,
292 #[serde(default)]
294 pub state_transition_history: bool,
295 #[serde(default)]
299 pub images: bool,
300 #[serde(default)]
304 pub audio: bool,
305 #[serde(default)]
309 pub files: bool,
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize)]
317#[serde(rename_all = "camelCase")]
318pub struct AgentSkill {
319 pub id: String,
321 pub name: String,
323 pub description: String,
325 #[serde(default, skip_serializing_if = "Vec::is_empty")]
327 pub tags: Vec<String>,
328 #[serde(default, skip_serializing_if = "Vec::is_empty")]
330 pub examples: Vec<String>,
331 #[serde(default, skip_serializing_if = "Vec::is_empty")]
333 pub input_modes: Vec<String>,
334 #[serde(default, skip_serializing_if = "Vec::is_empty")]
336 pub output_modes: Vec<String>,
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize)]
344#[serde(rename_all = "camelCase")]
345pub struct TaskStatusUpdateEvent {
346 #[serde(default = "kind_status_update")]
348 pub kind: String,
349 pub task_id: String,
351 #[serde(default, skip_serializing_if = "Option::is_none")]
353 pub context_id: Option<String>,
354 pub status: TaskStatus,
356 #[serde(rename = "final", default)]
358 pub is_final: bool,
359}
360
361fn kind_status_update() -> String {
362 "status-update".into()
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize)]
370#[serde(rename_all = "camelCase")]
371pub struct TaskArtifactUpdateEvent {
372 #[serde(default = "kind_artifact_update")]
374 pub kind: String,
375 pub task_id: String,
377 #[serde(default, skip_serializing_if = "Option::is_none")]
379 pub context_id: Option<String>,
380 pub artifact: Artifact,
382 #[serde(rename = "final", default)]
384 pub is_final: bool,
385}
386
387fn kind_artifact_update() -> String {
388 "artifact-update".into()
389}
390
391impl Part {
392 #[must_use]
403 pub fn text(s: impl Into<String>) -> Self {
404 Self::Text {
405 text: s.into(),
406 metadata: None,
407 }
408 }
409}
410
411impl Message {
412 #[must_use]
426 pub fn user_text(s: impl Into<String>) -> Self {
427 Self {
428 role: Role::User,
429 parts: vec![Part::text(s)],
430 message_id: None,
431 task_id: None,
432 context_id: None,
433 metadata: None,
434 }
435 }
436
437 #[must_use]
450 pub fn text_content(&self) -> Option<&str> {
451 self.parts.iter().find_map(|p| match p {
452 Part::Text { text, .. } => Some(text.as_str()),
453 _ => None,
454 })
455 }
456
457 #[must_use]
463 pub fn all_text_content(&self) -> String {
464 let parts: Vec<&str> = self
465 .parts
466 .iter()
467 .filter_map(|p| match p {
468 Part::Text { text, .. } => Some(text.as_str()),
469 _ => None,
470 })
471 .collect();
472 parts.join("\n\n")
473 }
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479
480 #[test]
481 fn task_state_serde() {
482 let states = [
483 (TaskState::Submitted, "\"submitted\""),
484 (TaskState::Working, "\"working\""),
485 (TaskState::InputRequired, "\"input-required\""),
486 (TaskState::Completed, "\"completed\""),
487 (TaskState::Failed, "\"failed\""),
488 (TaskState::Canceled, "\"canceled\""),
489 (TaskState::Rejected, "\"rejected\""),
490 (TaskState::AuthRequired, "\"auth-required\""),
491 (TaskState::Unknown, "\"unknown\""),
492 ];
493 for (state, expected) in states {
494 let json = serde_json::to_string(&state).unwrap();
495 assert_eq!(json, expected, "serialization mismatch for {state:?}");
496 let back: TaskState = serde_json::from_str(&json).unwrap();
497 assert_eq!(back, state);
498 }
499 }
500
501 #[test]
502 fn role_serde_lowercase() {
503 assert_eq!(serde_json::to_string(&Role::User).unwrap(), "\"user\"");
504 assert_eq!(serde_json::to_string(&Role::Agent).unwrap(), "\"agent\"");
505 }
506
507 #[test]
508 fn part_text_constructor() {
509 let part = Part::text("hello");
510 assert_eq!(
511 part,
512 Part::Text {
513 text: "hello".into(),
514 metadata: None
515 }
516 );
517 }
518
519 #[test]
520 fn part_kind_serde() {
521 let text_part = Part::text("hello");
522 let json = serde_json::to_string(&text_part).unwrap();
523 assert!(json.contains("\"kind\":\"text\""));
524 assert!(json.contains("\"text\":\"hello\""));
525 let back: Part = serde_json::from_str(&json).unwrap();
526 assert_eq!(back, text_part);
527
528 let file_part = Part::File {
529 file: FileContent {
530 name: Some("doc.pdf".into()),
531 media_type: None,
532 file_with_bytes: None,
533 file_with_uri: Some("https://example.com/doc.pdf".into()),
534 },
535 metadata: None,
536 };
537 let json = serde_json::to_string(&file_part).unwrap();
538 assert!(json.contains("\"kind\":\"file\""));
539 let back: Part = serde_json::from_str(&json).unwrap();
540 assert_eq!(back, file_part);
541
542 let data_part = Part::Data {
543 data: serde_json::json!({"key": "value"}),
544 metadata: None,
545 };
546 let json = serde_json::to_string(&data_part).unwrap();
547 assert!(json.contains("\"kind\":\"data\""));
548 let back: Part = serde_json::from_str(&json).unwrap();
549 assert_eq!(back, data_part);
550 }
551
552 #[test]
553 fn message_user_text_constructor() {
554 let msg = Message::user_text("test input");
555 assert_eq!(msg.role, Role::User);
556 assert_eq!(msg.text_content(), Some("test input"));
557 }
558
559 #[test]
560 fn message_serde_round_trip() {
561 let msg = Message::user_text("hello agent");
562 let json = serde_json::to_string(&msg).unwrap();
563 let back: Message = serde_json::from_str(&json).unwrap();
564 assert_eq!(back.role, Role::User);
565 assert_eq!(back.text_content(), Some("hello agent"));
566 }
567
568 #[test]
569 fn task_serde_round_trip() {
570 let task = Task {
571 id: "task-1".into(),
572 context_id: None,
573 status: TaskStatus {
574 state: TaskState::Working,
575 timestamp: "2025-01-01T00:00:00Z".into(),
576 message: None,
577 },
578 artifacts: vec![],
579 history: vec![Message::user_text("do something")],
580 metadata: None,
581 };
582 let json = serde_json::to_string(&task).unwrap();
583 assert!(json.contains("\"contextId\"").not());
584 let back: Task = serde_json::from_str(&json).unwrap();
585 assert_eq!(back.id, "task-1");
586 assert_eq!(back.status.state, TaskState::Working);
587 assert_eq!(back.history.len(), 1);
588 }
589
590 #[test]
591 fn task_skips_empty_vecs_and_none() {
592 let task = Task {
593 id: "t".into(),
594 context_id: None,
595 status: TaskStatus {
596 state: TaskState::Submitted,
597 timestamp: "ts".into(),
598 message: None,
599 },
600 artifacts: vec![],
601 history: vec![],
602 metadata: None,
603 };
604 let json = serde_json::to_string(&task).unwrap();
605 assert!(!json.contains("artifacts"));
606 assert!(!json.contains("history"));
607 assert!(!json.contains("metadata"));
608 assert!(!json.contains("contextId"));
609 }
610
611 #[test]
612 fn artifact_serde_round_trip() {
613 let artifact = Artifact {
614 artifact_id: "art-1".into(),
615 name: Some("result.txt".into()),
616 parts: vec![Part::text("file content")],
617 metadata: None,
618 };
619 let json = serde_json::to_string(&artifact).unwrap();
620 assert!(json.contains("\"artifactId\""));
621 let back: Artifact = serde_json::from_str(&json).unwrap();
622 assert_eq!(back.artifact_id, "art-1");
623 }
624
625 #[test]
626 fn agent_card_serde_round_trip() {
627 let card = AgentCard {
628 name: "test-agent".into(),
629 description: "A test agent".into(),
630 url: "http://localhost:8080".into(),
631 version: "0.1.0".into(),
632 protocol_version: "0.2.1".into(),
633 provider: Some(AgentProvider {
634 organization: "TestOrg".into(),
635 url: Some("https://test.org".into()),
636 }),
637 capabilities: AgentCapabilities {
638 streaming: true,
639 push_notifications: false,
640 state_transition_history: false,
641 images: false,
642 audio: false,
643 files: false,
644 },
645 default_input_modes: vec!["text".into()],
646 default_output_modes: vec!["text".into()],
647 skills: vec![AgentSkill {
648 id: "skill-1".into(),
649 name: "Test Skill".into(),
650 description: "Does testing".into(),
651 tags: vec!["test".into()],
652 examples: vec![],
653 input_modes: vec![],
654 output_modes: vec![],
655 }],
656 };
657 let json = serde_json::to_string_pretty(&card).unwrap();
658 let back: AgentCard = serde_json::from_str(&json).unwrap();
659 assert_eq!(back.name, "test-agent");
660 assert!(back.capabilities.streaming);
661 assert_eq!(back.skills.len(), 1);
662 }
663
664 #[test]
665 fn task_status_update_event_serde() {
666 let event = TaskStatusUpdateEvent {
667 kind: "status-update".into(),
668 task_id: "t-1".into(),
669 context_id: None,
670 status: TaskStatus {
671 state: TaskState::Completed,
672 timestamp: "ts".into(),
673 message: None,
674 },
675 is_final: true,
676 };
677 let json = serde_json::to_string(&event).unwrap();
678 assert!(json.contains("\"final\":true"));
679 assert!(!json.contains("isFinal"));
680 assert!(json.contains("\"kind\":\"status-update\""));
681 let back: TaskStatusUpdateEvent = serde_json::from_str(&json).unwrap();
682 assert!(back.is_final);
683 assert_eq!(back.kind, "status-update");
684 }
685
686 #[test]
687 fn task_artifact_update_event_serde() {
688 let event = TaskArtifactUpdateEvent {
689 kind: "artifact-update".into(),
690 task_id: "t-1".into(),
691 context_id: None,
692 artifact: Artifact {
693 artifact_id: "a-1".into(),
694 name: None,
695 parts: vec![Part::text("data")],
696 metadata: None,
697 },
698 is_final: false,
699 };
700 let json = serde_json::to_string(&event).unwrap();
701 assert!(json.contains("\"final\":false"));
702 assert!(json.contains("\"kind\":\"artifact-update\""));
703 let back: TaskArtifactUpdateEvent = serde_json::from_str(&json).unwrap();
704 assert!(!back.is_final);
705 assert_eq!(back.kind, "artifact-update");
706 }
707
708 #[test]
709 fn file_content_serde() {
710 let fc = FileContent {
711 name: Some("doc.pdf".into()),
712 media_type: Some("application/pdf".into()),
713 file_with_bytes: Some("base64data==".into()),
714 file_with_uri: None,
715 };
716 let json = serde_json::to_string(&fc).unwrap();
717 assert!(json.contains("\"mediaType\""));
718 assert!(json.contains("\"fileWithBytes\""));
719 assert!(!json.contains("fileWithUri"));
720 let back: FileContent = serde_json::from_str(&json).unwrap();
721 assert_eq!(back.name.as_deref(), Some("doc.pdf"));
722 }
723
724 #[test]
725 fn all_text_content_single_part() {
726 let msg = Message::user_text("hello world");
727 assert_eq!(msg.all_text_content(), "hello world");
728 }
729
730 #[test]
731 fn all_text_content_multiple_parts_joined() {
732 let msg = Message {
733 role: Role::User,
734 parts: vec![
735 Part::text("first"),
736 Part::text("second"),
737 Part::text("third"),
738 ],
739 message_id: None,
740 task_id: None,
741 context_id: None,
742 metadata: None,
743 };
744 assert_eq!(msg.all_text_content(), "first\n\nsecond\n\nthird");
745 }
746
747 #[test]
748 fn all_text_content_no_text_parts_returns_empty() {
749 let msg = Message {
750 role: Role::User,
751 parts: vec![],
752 message_id: None,
753 task_id: None,
754 context_id: None,
755 metadata: None,
756 };
757 assert_eq!(msg.all_text_content(), "");
758 }
759
760 #[test]
761 fn all_text_content_skips_non_text_parts() {
762 let msg = Message {
763 role: Role::User,
764 parts: vec![
765 Part::text("text-only"),
766 Part::Data {
767 data: serde_json::json!({"key": "val"}),
768 metadata: None,
769 },
770 ],
771 message_id: None,
772 task_id: None,
773 context_id: None,
774 metadata: None,
775 };
776 assert_eq!(msg.all_text_content(), "text-only");
777 }
778
779 #[test]
780 fn agent_capabilities_default_has_no_modalities() {
781 let caps = AgentCapabilities::default();
782 assert!(!caps.images);
783 assert!(!caps.audio);
784 assert!(!caps.files);
785 }
786
787 #[test]
788 fn agent_capabilities_modality_fields_serialize() {
789 let caps = AgentCapabilities {
790 streaming: false,
791 push_notifications: false,
792 state_transition_history: false,
793 images: false,
794 audio: false,
795 files: false,
796 };
797 let json = serde_json::to_string(&caps).unwrap();
798 assert!(json.contains("\"images\":false"));
799 assert!(json.contains("\"audio\":false"));
800 assert!(json.contains("\"files\":false"));
801 }
802
803 #[test]
804 fn deserialize_legacy_capabilities_uses_modality_defaults() {
805 let json = r#"{"streaming": true}"#;
807 let caps: AgentCapabilities = serde_json::from_str(json).unwrap();
808 assert!(caps.streaming);
809 assert!(!caps.images);
810 assert!(!caps.audio);
811 assert!(!caps.files);
812 }
813
814 trait Not {
815 fn not(&self) -> bool;
816 }
817 impl Not for bool {
818 fn not(&self) -> bool {
819 !*self
820 }
821 }
822}