use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TaskState {
#[serde(rename = "submitted")]
Submitted,
#[serde(rename = "working")]
Working,
#[serde(rename = "input-required")]
InputRequired,
#[serde(rename = "completed")]
Completed,
#[serde(rename = "failed")]
Failed,
#[serde(rename = "canceled")]
Canceled,
#[serde(rename = "rejected")]
Rejected,
#[serde(rename = "auth-required")]
AuthRequired,
#[serde(rename = "unknown")]
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Task {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context_id: Option<String>,
pub status: TaskStatus,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub artifacts: Vec<Artifact>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub history: Vec<Message>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskStatus {
pub state: TaskState,
pub timestamp: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<Message>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
User,
Agent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Message {
pub role: Role,
pub parts: Vec<Part>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub task_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum Part {
Text {
text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
metadata: Option<serde_json::Value>,
},
File {
file: FileContent,
#[serde(default, skip_serializing_if = "Option::is_none")]
metadata: Option<serde_json::Value>,
},
Data {
data: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
metadata: Option<serde_json::Value>,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FileContent {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub media_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub file_with_bytes: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub file_with_uri: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Artifact {
pub artifact_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub parts: Vec<Part>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentCard {
pub name: String,
pub description: String,
pub url: String,
pub version: String,
pub protocol_version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider: Option<AgentProvider>,
pub capabilities: AgentCapabilities,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub default_input_modes: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub default_output_modes: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub skills: Vec<AgentSkill>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentProvider {
pub organization: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[allow(clippy::struct_excessive_bools)]
pub struct AgentCapabilities {
#[serde(default)]
pub streaming: bool,
#[serde(default)]
pub push_notifications: bool,
#[serde(default)]
pub state_transition_history: bool,
#[serde(default)]
pub images: bool,
#[serde(default)]
pub audio: bool,
#[serde(default)]
pub files: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentSkill {
pub id: String,
pub name: String,
pub description: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub examples: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub input_modes: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub output_modes: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskStatusUpdateEvent {
#[serde(default = "kind_status_update")]
pub kind: String,
pub task_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context_id: Option<String>,
pub status: TaskStatus,
#[serde(rename = "final", default)]
pub is_final: bool,
}
fn kind_status_update() -> String {
"status-update".into()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskArtifactUpdateEvent {
#[serde(default = "kind_artifact_update")]
pub kind: String,
pub task_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context_id: Option<String>,
pub artifact: Artifact,
#[serde(rename = "final", default)]
pub is_final: bool,
}
fn kind_artifact_update() -> String {
"artifact-update".into()
}
impl Part {
#[must_use]
pub fn text(s: impl Into<String>) -> Self {
Self::Text {
text: s.into(),
metadata: None,
}
}
}
impl Message {
#[must_use]
pub fn user_text(s: impl Into<String>) -> Self {
Self {
role: Role::User,
parts: vec![Part::text(s)],
message_id: None,
task_id: None,
context_id: None,
metadata: None,
}
}
#[must_use]
pub fn text_content(&self) -> Option<&str> {
self.parts.iter().find_map(|p| match p {
Part::Text { text, .. } => Some(text.as_str()),
_ => None,
})
}
#[must_use]
pub fn all_text_content(&self) -> String {
let parts: Vec<&str> = self
.parts
.iter()
.filter_map(|p| match p {
Part::Text { text, .. } => Some(text.as_str()),
_ => None,
})
.collect();
parts.join("\n\n")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn task_state_serde() {
let states = [
(TaskState::Submitted, "\"submitted\""),
(TaskState::Working, "\"working\""),
(TaskState::InputRequired, "\"input-required\""),
(TaskState::Completed, "\"completed\""),
(TaskState::Failed, "\"failed\""),
(TaskState::Canceled, "\"canceled\""),
(TaskState::Rejected, "\"rejected\""),
(TaskState::AuthRequired, "\"auth-required\""),
(TaskState::Unknown, "\"unknown\""),
];
for (state, expected) in states {
let json = serde_json::to_string(&state).unwrap();
assert_eq!(json, expected, "serialization mismatch for {state:?}");
let back: TaskState = serde_json::from_str(&json).unwrap();
assert_eq!(back, state);
}
}
#[test]
fn role_serde_lowercase() {
assert_eq!(serde_json::to_string(&Role::User).unwrap(), "\"user\"");
assert_eq!(serde_json::to_string(&Role::Agent).unwrap(), "\"agent\"");
}
#[test]
fn part_text_constructor() {
let part = Part::text("hello");
assert_eq!(
part,
Part::Text {
text: "hello".into(),
metadata: None
}
);
}
#[test]
fn part_kind_serde() {
let text_part = Part::text("hello");
let json = serde_json::to_string(&text_part).unwrap();
assert!(json.contains("\"kind\":\"text\""));
assert!(json.contains("\"text\":\"hello\""));
let back: Part = serde_json::from_str(&json).unwrap();
assert_eq!(back, text_part);
let file_part = Part::File {
file: FileContent {
name: Some("doc.pdf".into()),
media_type: None,
file_with_bytes: None,
file_with_uri: Some("https://example.com/doc.pdf".into()),
},
metadata: None,
};
let json = serde_json::to_string(&file_part).unwrap();
assert!(json.contains("\"kind\":\"file\""));
let back: Part = serde_json::from_str(&json).unwrap();
assert_eq!(back, file_part);
let data_part = Part::Data {
data: serde_json::json!({"key": "value"}),
metadata: None,
};
let json = serde_json::to_string(&data_part).unwrap();
assert!(json.contains("\"kind\":\"data\""));
let back: Part = serde_json::from_str(&json).unwrap();
assert_eq!(back, data_part);
}
#[test]
fn message_user_text_constructor() {
let msg = Message::user_text("test input");
assert_eq!(msg.role, Role::User);
assert_eq!(msg.text_content(), Some("test input"));
}
#[test]
fn message_serde_round_trip() {
let msg = Message::user_text("hello agent");
let json = serde_json::to_string(&msg).unwrap();
let back: Message = serde_json::from_str(&json).unwrap();
assert_eq!(back.role, Role::User);
assert_eq!(back.text_content(), Some("hello agent"));
}
#[test]
fn task_serde_round_trip() {
let task = Task {
id: "task-1".into(),
context_id: None,
status: TaskStatus {
state: TaskState::Working,
timestamp: "2025-01-01T00:00:00Z".into(),
message: None,
},
artifacts: vec![],
history: vec![Message::user_text("do something")],
metadata: None,
};
let json = serde_json::to_string(&task).unwrap();
assert!(json.contains("\"contextId\"").not());
let back: Task = serde_json::from_str(&json).unwrap();
assert_eq!(back.id, "task-1");
assert_eq!(back.status.state, TaskState::Working);
assert_eq!(back.history.len(), 1);
}
#[test]
fn task_skips_empty_vecs_and_none() {
let task = Task {
id: "t".into(),
context_id: None,
status: TaskStatus {
state: TaskState::Submitted,
timestamp: "ts".into(),
message: None,
},
artifacts: vec![],
history: vec![],
metadata: None,
};
let json = serde_json::to_string(&task).unwrap();
assert!(!json.contains("artifacts"));
assert!(!json.contains("history"));
assert!(!json.contains("metadata"));
assert!(!json.contains("contextId"));
}
#[test]
fn artifact_serde_round_trip() {
let artifact = Artifact {
artifact_id: "art-1".into(),
name: Some("result.txt".into()),
parts: vec![Part::text("file content")],
metadata: None,
};
let json = serde_json::to_string(&artifact).unwrap();
assert!(json.contains("\"artifactId\""));
let back: Artifact = serde_json::from_str(&json).unwrap();
assert_eq!(back.artifact_id, "art-1");
}
#[test]
fn agent_card_serde_round_trip() {
let card = AgentCard {
name: "test-agent".into(),
description: "A test agent".into(),
url: "http://localhost:8080".into(),
version: "0.1.0".into(),
protocol_version: "0.2.1".into(),
provider: Some(AgentProvider {
organization: "TestOrg".into(),
url: Some("https://test.org".into()),
}),
capabilities: AgentCapabilities {
streaming: true,
push_notifications: false,
state_transition_history: false,
images: false,
audio: false,
files: false,
},
default_input_modes: vec!["text".into()],
default_output_modes: vec!["text".into()],
skills: vec![AgentSkill {
id: "skill-1".into(),
name: "Test Skill".into(),
description: "Does testing".into(),
tags: vec!["test".into()],
examples: vec![],
input_modes: vec![],
output_modes: vec![],
}],
};
let json = serde_json::to_string_pretty(&card).unwrap();
let back: AgentCard = serde_json::from_str(&json).unwrap();
assert_eq!(back.name, "test-agent");
assert!(back.capabilities.streaming);
assert_eq!(back.skills.len(), 1);
}
#[test]
fn task_status_update_event_serde() {
let event = TaskStatusUpdateEvent {
kind: "status-update".into(),
task_id: "t-1".into(),
context_id: None,
status: TaskStatus {
state: TaskState::Completed,
timestamp: "ts".into(),
message: None,
},
is_final: true,
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("\"final\":true"));
assert!(!json.contains("isFinal"));
assert!(json.contains("\"kind\":\"status-update\""));
let back: TaskStatusUpdateEvent = serde_json::from_str(&json).unwrap();
assert!(back.is_final);
assert_eq!(back.kind, "status-update");
}
#[test]
fn task_artifact_update_event_serde() {
let event = TaskArtifactUpdateEvent {
kind: "artifact-update".into(),
task_id: "t-1".into(),
context_id: None,
artifact: Artifact {
artifact_id: "a-1".into(),
name: None,
parts: vec![Part::text("data")],
metadata: None,
},
is_final: false,
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("\"final\":false"));
assert!(json.contains("\"kind\":\"artifact-update\""));
let back: TaskArtifactUpdateEvent = serde_json::from_str(&json).unwrap();
assert!(!back.is_final);
assert_eq!(back.kind, "artifact-update");
}
#[test]
fn file_content_serde() {
let fc = FileContent {
name: Some("doc.pdf".into()),
media_type: Some("application/pdf".into()),
file_with_bytes: Some("base64data==".into()),
file_with_uri: None,
};
let json = serde_json::to_string(&fc).unwrap();
assert!(json.contains("\"mediaType\""));
assert!(json.contains("\"fileWithBytes\""));
assert!(!json.contains("fileWithUri"));
let back: FileContent = serde_json::from_str(&json).unwrap();
assert_eq!(back.name.as_deref(), Some("doc.pdf"));
}
#[test]
fn all_text_content_single_part() {
let msg = Message::user_text("hello world");
assert_eq!(msg.all_text_content(), "hello world");
}
#[test]
fn all_text_content_multiple_parts_joined() {
let msg = Message {
role: Role::User,
parts: vec![
Part::text("first"),
Part::text("second"),
Part::text("third"),
],
message_id: None,
task_id: None,
context_id: None,
metadata: None,
};
assert_eq!(msg.all_text_content(), "first\n\nsecond\n\nthird");
}
#[test]
fn all_text_content_no_text_parts_returns_empty() {
let msg = Message {
role: Role::User,
parts: vec![],
message_id: None,
task_id: None,
context_id: None,
metadata: None,
};
assert_eq!(msg.all_text_content(), "");
}
#[test]
fn all_text_content_skips_non_text_parts() {
let msg = Message {
role: Role::User,
parts: vec![
Part::text("text-only"),
Part::Data {
data: serde_json::json!({"key": "val"}),
metadata: None,
},
],
message_id: None,
task_id: None,
context_id: None,
metadata: None,
};
assert_eq!(msg.all_text_content(), "text-only");
}
#[test]
fn agent_capabilities_default_has_no_modalities() {
let caps = AgentCapabilities::default();
assert!(!caps.images);
assert!(!caps.audio);
assert!(!caps.files);
}
#[test]
fn agent_capabilities_modality_fields_serialize() {
let caps = AgentCapabilities {
streaming: false,
push_notifications: false,
state_transition_history: false,
images: false,
audio: false,
files: false,
};
let json = serde_json::to_string(&caps).unwrap();
assert!(json.contains("\"images\":false"));
assert!(json.contains("\"audio\":false"));
assert!(json.contains("\"files\":false"));
}
#[test]
fn deserialize_legacy_capabilities_uses_modality_defaults() {
let json = r#"{"streaming": true}"#;
let caps: AgentCapabilities = serde_json::from_str(json).unwrap();
assert!(caps.streaming);
assert!(!caps.images);
assert!(!caps.audio);
assert!(!caps.files);
}
trait Not {
fn not(&self) -> bool;
}
impl Not for bool {
fn not(&self) -> bool {
!*self
}
}
}