#![allow(non_snake_case)]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
User,
Agent,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum TaskStatus {
Submitted,
Working,
InputRequired,
Completed,
Failed,
Canceled,
Rejected,
Authenticated,
#[serde(other)]
Unknown,
}
impl TaskStatus {
pub fn is_terminal(&self) -> bool {
matches!(
self,
TaskStatus::Completed
| TaskStatus::Failed
| TaskStatus::Canceled
| TaskStatus::Rejected
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
#[non_exhaustive]
pub enum Part {
Text {
content: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
metadata: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
mediaType: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
filename: Option<String>,
},
Raw {
bytes: String,
mediaType: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
metadata: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
filename: Option<String>,
},
Url {
url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
mediaType: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
metadata: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
filename: Option<String>,
},
Data {
data: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
metadata: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
mediaType: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
filename: Option<String>,
},
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub messageId: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub contextId: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub taskId: Option<String>,
pub role: Role,
pub parts: Vec<Part>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
#[serde(default)]
pub extensions: Vec<String>,
#[serde(default)]
pub referenceTaskIds: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Artifact {
pub artifactId: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub parts: Vec<Part>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
#[serde(default)]
pub extensions: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Task {
pub id: String,
pub contextId: String,
pub status: TaskStatus,
#[serde(default)]
pub artifacts: Vec<Artifact>,
#[serde(default)]
pub history: Vec<Message>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentProvider {
pub organization: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentCapabilities {
#[serde(default)]
pub streaming: bool,
#[serde(default)]
pub pushNotifications: bool,
#[serde(default)]
pub stateTransitionHistory: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentSkill {
pub id: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default)]
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentInterface {
pub transport: String,
pub url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentCardSignature {
pub alg: String,
pub kid: String,
pub signature: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentCard {
pub id: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider: Option<AgentProvider>,
pub capabilities: AgentCapabilities,
#[serde(default)]
pub skills: Vec<AgentSkill>,
#[serde(default)]
pub interfaces: Vec<AgentInterface>,
#[serde(default)]
pub securitySchemes: serde_json::Map<String, serde_json::Value>,
#[serde(default)]
pub security: Vec<serde_json::Value>,
#[serde(default)]
pub extensions: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature: Option<AgentCardSignature>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SendMessageParams {
pub message: Message,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub configuration: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SendMessageResult {
Message(Message),
Task(Task),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetTaskParams {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub historyLength: Option<usize>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ListTasksParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub contextId: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<usize>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ListTasksResult {
pub tasks: Vec<Task>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nextCursor: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CancelTaskParams {
pub id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubscribeToTaskParams {
pub id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PushNotificationConfigParams {
pub taskId: String,
pub url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub token: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PushNotificationConfig {
pub id: String,
pub taskId: String,
pub url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetPushNotificationConfigParams {
pub taskId: String,
pub id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListPushNotificationConfigsParams {
pub taskId: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ListPushNotificationConfigsResult {
pub configs: Vec<PushNotificationConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeletePushNotificationConfigParams {
pub taskId: String,
pub id: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GetExtendedAgentCardParams {}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn unknown_task_status_deserialises_to_unknown_not_error() {
let s: TaskStatus = serde_json::from_value(json!("some_future_state")).unwrap();
assert_eq!(s, TaskStatus::Unknown);
assert!(!s.is_terminal(), "Unknown status is never terminal");
}
#[test]
fn unknown_part_type_deserialises_to_unknown_not_error() {
let p: Part = serde_json::from_value(json!({"type": "future_part", "x": 1})).unwrap();
assert!(matches!(p, Part::Unknown));
}
#[test]
fn part_text_serialises_with_type_text() {
let p = Part::Text {
content: "hello".into(),
metadata: None,
mediaType: None,
filename: None,
};
let v = serde_json::to_value(&p).unwrap();
assert_eq!(v["type"], "text");
assert_eq!(v["content"], "hello");
}
#[test]
fn part_raw_uses_base64_string_field() {
let p = Part::Raw {
bytes: "aGVsbG8=".into(),
mediaType: "text/plain".into(),
metadata: None,
filename: None,
};
let v = serde_json::to_value(&p).unwrap();
assert_eq!(v["type"], "raw");
assert_eq!(v["bytes"], "aGVsbG8=");
assert_eq!(v["mediaType"], "text/plain");
}
#[test]
fn message_round_trips() {
let m = Message {
messageId: "m-1".into(),
contextId: Some("c-1".into()),
taskId: Some("t-1".into()),
role: Role::User,
parts: vec![Part::Text {
content: "hi".into(),
metadata: None,
mediaType: None,
filename: None,
}],
metadata: None,
extensions: vec![],
referenceTaskIds: vec![],
};
let v = serde_json::to_value(&m).unwrap();
let back: Message = serde_json::from_value(v).unwrap();
assert_eq!(back.messageId, "m-1");
assert_eq!(back.role, Role::User);
assert_eq!(back.parts.len(), 1);
}
#[test]
fn task_with_empty_history_serialises_minimally() {
let t = Task {
id: "t-1".into(),
contextId: "c-1".into(),
status: TaskStatus::Submitted,
artifacts: vec![],
history: vec![],
metadata: None,
};
let v = serde_json::to_value(&t).unwrap();
assert_eq!(v["id"], "t-1");
assert_eq!(v["status"], "submitted");
}
#[test]
fn agent_card_round_trips() {
let card = AgentCard {
id: "agent-1".into(),
name: "Echo".into(),
description: Some("Echoes input".into()),
provider: None,
capabilities: AgentCapabilities {
streaming: false,
pushNotifications: false,
stateTransitionHistory: false,
},
skills: vec![],
interfaces: vec![],
securitySchemes: serde_json::Map::new(),
security: vec![],
extensions: vec![],
signature: None,
};
let v = serde_json::to_value(&card).unwrap();
let back: AgentCard = serde_json::from_value(v).unwrap();
assert_eq!(back.id, "agent-1");
}
#[test]
fn role_agent_serialises_as_lowercase() {
let v = serde_json::to_value(Role::Agent).unwrap();
assert_eq!(v, json!("agent"));
}
#[test]
fn task_status_is_terminal_covers_completed_failed_canceled_rejected() {
assert!(TaskStatus::Completed.is_terminal());
assert!(TaskStatus::Failed.is_terminal());
assert!(TaskStatus::Canceled.is_terminal());
assert!(TaskStatus::Rejected.is_terminal());
}
#[test]
fn task_status_is_terminal_excludes_in_progress() {
assert!(!TaskStatus::Submitted.is_terminal());
assert!(!TaskStatus::Working.is_terminal());
assert!(!TaskStatus::Authenticated.is_terminal());
assert!(!TaskStatus::InputRequired.is_terminal());
}
}