use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
#[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 preferred_transport: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub documentation_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub icon_url: Option<String>,
pub provider: AgentProvider,
pub capabilities: AgentCapabilities,
pub default_input_modes: Vec<String>,
pub default_output_modes: Vec<String>,
pub skills: Vec<AgentSkill>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub supported_interfaces: Vec<AgentInterface>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub additional_interfaces: Vec<AgentInterface>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub security_schemes: HashMap<String, SecurityScheme>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub security_requirements: Vec<SecurityRequirement>,
#[serde(default)]
pub supports_authenticated_extended_card: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub signatures: Vec<AgentCardSignature>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentCardSignature {
pub protected: String,
pub signature: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub header: Option<Value>,
}
impl Default for AgentCard {
fn default() -> Self {
Self {
name: String::new(),
description: String::new(),
url: String::new(),
version: "1.0.0".into(),
protocol_version: "1.0".into(),
preferred_transport: Some("JSONRPC".into()),
documentation_url: None,
icon_url: None,
provider: AgentProvider {
organization: String::new(),
url: None,
},
capabilities: AgentCapabilities::default(),
default_input_modes: Vec::new(),
default_output_modes: Vec::new(),
skills: Vec::new(),
supported_interfaces: Vec::new(),
additional_interfaces: Vec::new(),
security_schemes: HashMap::new(),
security_requirements: Vec::new(),
supports_authenticated_extended_card: false,
signatures: Vec::new(),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SecurityRequirement {
#[serde(default)]
pub schemes: HashMap<String, StringList>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct StringList {
#[serde(default)]
pub list: Vec<String>,
}
#[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, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentCapabilities {
pub streaming: bool,
pub push_notifications: bool,
pub state_transition_history: bool,
#[serde(default)]
pub extended_agent_card: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub extensions: Vec<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentSkill {
pub id: String,
pub name: String,
pub description: String,
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 AgentInterface {
pub url: String,
#[serde(default = "default_protocol_binding")]
pub protocol_binding: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transport: Option<TransportProtocol>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tenant: Option<String>,
#[serde(default = "default_interface_protocol_version")]
pub protocol_version: String,
}
fn default_protocol_binding() -> String {
"JSONRPC".to_string()
}
fn default_interface_protocol_version() -> String {
"1.0".to_string()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum TransportProtocol {
#[serde(rename = "JSONRPC")]
JsonRpc,
#[serde(rename = "GRPC")]
Grpc,
#[serde(rename = "HTTP+JSON")]
HttpJson,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum SecurityScheme {
#[serde(rename = "apiKey")]
ApiKey {
#[serde(rename = "in")]
location: String,
name: String,
},
#[serde(rename = "http")]
Http { scheme: String },
#[serde(rename = "oauth2")]
OAuth2 { flows: Value },
#[serde(rename = "openIdConnect")]
OpenIdConnect {
#[serde(rename = "openIdConnectUrl")]
open_id_connect_url: String,
},
#[serde(rename = "mutualTLS")]
MutualTls,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum TaskState {
Submitted,
Working,
InputRequired,
AuthRequired,
Completed,
Failed,
Canceled,
Rejected,
}
impl TaskState {
pub fn is_terminal(self) -> bool {
matches!(
self,
TaskState::Completed | TaskState::Failed | TaskState::Canceled | TaskState::Rejected
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Task {
pub id: String,
pub context_id: String,
pub status: TaskStatus,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub history: Vec<Message>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub artifacts: Vec<Artifact>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskStatus {
pub state: TaskState,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<Message>,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MessageRole {
User,
Agent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Message {
pub message_id: String,
pub role: MessageRole,
pub parts: Vec<Part>,
#[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 = "HashMap::is_empty")]
pub metadata: HashMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "camelCase")]
pub enum Part {
#[serde(rename = "text")]
Text(TextPart),
#[serde(rename = "file")]
File(FilePart),
#[serde(rename = "data")]
Data(DataPart),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextPart {
pub text: String,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FilePart {
pub file: FileContent,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, Value>,
}
#[derive(Debug, Clone, 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 mime_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bytes: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub uri: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataPart {
pub data: Value,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Artifact {
pub artifact_id: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub parts: Vec<Part>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SendMessageParams {
pub message: Message,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub configuration: Option<MessageConfiguration>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MessageConfiguration {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub accepted_output_modes: Vec<String>,
#[serde(default)]
pub blocking: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub history_length: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SendMessageResult {
Task(Task),
Message(Message),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetTaskParams {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub history_length: Option<u32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListTasksParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state: Option<TaskState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListTasksResult {
pub tasks: Vec<Task>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PushNotificationConfig {
pub url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub token: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authentication: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PushNotificationConfigParams {
pub task_id: String,
pub config: PushNotificationConfig,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn task_state_serializes_kebab_case() {
assert_eq!(
serde_json::to_string(&TaskState::InputRequired).unwrap(),
"\"input-required\""
);
assert_eq!(
serde_json::to_string(&TaskState::Submitted).unwrap(),
"\"submitted\""
);
assert_eq!(
serde_json::to_string(&TaskState::AuthRequired).unwrap(),
"\"auth-required\""
);
}
#[test]
fn part_uses_kind_discriminator() {
let part = Part::Text(TextPart {
text: "hi".into(),
metadata: HashMap::new(),
});
let v = serde_json::to_value(&part).unwrap();
assert_eq!(v["kind"], "text");
assert_eq!(v["text"], "hi");
}
#[test]
fn message_round_trips_camel_case() {
let msg = Message {
message_id: "m-1".into(),
role: MessageRole::User,
parts: vec![Part::Text(TextPart {
text: "hello".into(),
metadata: HashMap::new(),
})],
task_id: Some("t-1".into()),
context_id: None,
metadata: HashMap::new(),
};
let v = serde_json::to_value(&msg).unwrap();
assert_eq!(v["messageId"], "m-1");
assert_eq!(v["taskId"], "t-1");
let parsed: Message = serde_json::from_value(v).unwrap();
assert_eq!(parsed.message_id, "m-1");
assert_eq!(parsed.role, MessageRole::User);
}
#[test]
fn agent_card_emits_camel_case() {
let card = AgentCard {
name: "CAR".into(),
description: "test".into(),
url: "https://example.com".into(),
version: "1.0.0".into(),
protocol_version: "1.0".into(),
preferred_transport: Some("JSONRPC".into()),
provider: AgentProvider {
organization: "Parslee".into(),
url: None,
},
capabilities: AgentCapabilities {
streaming: true,
push_notifications: false,
state_transition_history: true,
extended_agent_card: false,
extensions: Vec::new(),
},
default_input_modes: vec!["text".into()],
default_output_modes: vec!["text".into()],
skills: vec![],
documentation_url: None,
icon_url: None,
supported_interfaces: vec![],
additional_interfaces: vec![AgentInterface {
url: "wss://example.com".into(),
protocol_binding: "JSONRPC".into(),
transport: Some(TransportProtocol::JsonRpc),
tenant: None,
protocol_version: "1.0".into(),
}],
security_schemes: HashMap::new(),
supports_authenticated_extended_card: false,
security_requirements: vec![],
signatures: vec![],
};
let v = serde_json::to_value(&card).unwrap();
assert_eq!(v["defaultInputModes"][0], "text");
assert_eq!(v["capabilities"]["pushNotifications"], false);
assert_eq!(v["additionalInterfaces"][0]["transport"], "JSONRPC");
}
}