use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub const JSONRPC_VERSION: &str = "2.0";
pub const METHOD_SEND: &str = "tasks/send";
pub const METHOD_SEND_SUBSCRIBE: &str = "tasks/sendSubscribe";
pub const METHOD_GET: &str = "tasks/get";
pub const METHOD_CANCEL: &str = "tasks/cancel";
pub const ERROR_CODE_PARSE: i64 = -32700;
pub const ERROR_CODE_METHOD_NOT_FOUND: i64 = -32601;
pub const ERROR_CODE_INVALID_PARAMS: i64 = -32602;
pub const ERROR_CODE_TASK_FAILED: i64 = -32000;
pub const ERROR_CODE_TASK_NOT_FOUND: i64 = -32001;
pub const ERROR_CODE_TERMINAL_STATE: i64 = -32002;
pub const ERROR_CODE_INVALID_TRANSITION: i64 = -32003;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentCard {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub provider: Option<AgentProvider>,
#[serde(default)]
pub skills: Vec<AgentSkill>,
#[serde(default = "default_content_types")]
pub default_input_modes: Vec<String>,
#[serde(default = "default_content_types")]
pub default_output_modes: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub authentication: Option<AgentAuthentication>,
#[serde(default)]
pub capabilities: AgentCapabilities,
}
fn default_content_types() -> Vec<String> {
vec!["text/plain".to_string()]
}
impl AgentCard {
pub fn builder(name: impl Into<String>, url: impl Into<String>) -> AgentCardBuilder {
AgentCardBuilder {
name: name.into(),
url: url.into(),
description: None,
version: None,
provider: None,
skills: Vec::new(),
default_input_modes: default_content_types(),
default_output_modes: default_content_types(),
authentication: None,
capabilities: AgentCapabilities::default(),
}
}
pub fn from_agent(agent: &dyn crate::agent::Agent, url: impl Into<String>) -> Self {
let skills: Vec<AgentSkill> = agent
.tool_definitions()
.into_iter()
.map(|td| AgentSkill::new(&td.function.name, &td.function.description))
.collect();
AgentCard {
name: agent.name().to_string(),
description: Some(agent.system_prompt().to_string()),
url: url.into(),
version: Some("1.0.0".to_string()),
provider: None,
skills,
default_input_modes: default_content_types(),
default_output_modes: default_content_types(),
authentication: None,
capabilities: AgentCapabilities::default(),
}
}
}
pub struct AgentCardBuilder {
name: String,
url: String,
description: Option<String>,
version: Option<String>,
provider: Option<AgentProvider>,
skills: Vec<AgentSkill>,
default_input_modes: Vec<String>,
default_output_modes: Vec<String>,
authentication: Option<AgentAuthentication>,
capabilities: AgentCapabilities,
}
impl AgentCardBuilder {
pub fn description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn version(mut self, version: impl Into<String>) -> Self {
self.version = Some(version.into());
self
}
pub fn provider(mut self, provider: AgentProvider) -> Self {
self.provider = Some(provider);
self
}
pub fn skill(mut self, skill: AgentSkill) -> Self {
self.skills.push(skill);
self
}
pub fn skills(mut self, skills: Vec<AgentSkill>) -> Self {
self.skills.extend(skills);
self
}
pub fn input_modes(mut self, modes: Vec<impl Into<String>>) -> Self {
self.default_input_modes = modes.into_iter().map(|m| m.into()).collect();
self
}
pub fn output_modes(mut self, modes: Vec<impl Into<String>>) -> Self {
self.default_output_modes = modes.into_iter().map(|m| m.into()).collect();
self
}
pub fn authentication(mut self, auth: AgentAuthentication) -> Self {
self.authentication = Some(auth);
self
}
pub fn streaming(mut self) -> Self {
self.capabilities.streaming = true;
self
}
pub fn push_notifications(mut self) -> Self {
self.capabilities.push_notifications = true;
self
}
pub fn build(self) -> AgentCard {
AgentCard {
name: self.name,
description: self.description,
url: self.url,
version: self.version,
provider: self.provider,
skills: self.skills,
default_input_modes: self.default_input_modes,
default_output_modes: self.default_output_modes,
authentication: self.authentication,
capabilities: self.capabilities,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentProvider {
pub organization: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
}
impl AgentProvider {
pub fn new(organization: impl Into<String>) -> Self {
Self {
organization: organization.into(),
url: None,
}
}
pub fn with_url(mut self, url: impl Into<String>) -> Self {
self.url = Some(url.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentSkill {
pub id: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<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>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
}
impl AgentSkill {
pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
let name_str: String = name.into();
Self {
id: name_str.clone(),
name: name_str,
description: Some(description.into()),
examples: Vec::new(),
input_modes: Vec::new(),
output_modes: Vec::new(),
tags: Vec::new(),
}
}
pub fn with_examples(mut self, examples: Vec<impl Into<String>>) -> Self {
self.examples = examples.into_iter().map(|e| e.into()).collect();
self
}
pub fn with_tags(mut self, tags: Vec<impl Into<String>>) -> Self {
self.tags = tags.into_iter().map(|t| t.into()).collect();
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentAuthentication {
pub schemes: Vec<AuthenticationScheme>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthenticationScheme {
pub scheme: String,
#[serde(flatten)]
pub config: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentCapabilities {
#[serde(default)]
pub streaming: bool,
#[serde(default)]
pub push_notifications: bool,
#[serde(default)]
pub state_transition_history: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum TaskState {
Submitted,
Working,
InputRequired,
Completed,
Failed,
Canceled,
}
impl TaskState {
pub fn is_terminal(self) -> bool {
matches!(self, Self::Completed | Self::Failed | Self::Canceled)
}
pub fn can_transition_to(self, next: Self) -> bool {
if self.is_terminal() {
return false;
}
matches!(
(self, next),
(Self::Submitted, Self::Working)
| (Self::Submitted, Self::Canceled)
| (Self::Working, Self::Completed)
| (Self::Working, Self::Failed)
| (Self::Working, Self::InputRequired)
| (Self::Working, Self::Canceled)
| (Self::InputRequired, Self::Working)
| (Self::InputRequired, Self::Canceled)
)
}
}
impl std::fmt::Display for TaskState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Submitted => write!(f, "submitted"),
Self::Working => write!(f, "working"),
Self::InputRequired => write!(f, "input-required"),
Self::Completed => write!(f, "completed"),
Self::Failed => write!(f, "failed"),
Self::Canceled => write!(f, "canceled"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct A2ATaskRequest {
pub jsonrpc: String,
pub id: String,
pub method: String,
pub params: A2ATaskParams,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct A2ATaskParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
pub message: A2AMessage,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct A2AMessage {
pub role: String,
pub parts: Vec<A2APart>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum A2APart {
#[serde(rename = "text")]
Text {
text: String,
},
#[serde(rename = "file")]
File {
#[serde(rename = "mimeType")]
mime_type: String,
data: String,
},
}
impl A2AMessage {
pub fn user_text(text: impl Into<String>) -> Self {
Self {
role: "user".to_string(),
parts: vec![A2APart::Text { text: text.into() }],
}
}
pub fn agent_text(text: impl Into<String>) -> Self {
Self {
role: "agent".to_string(),
parts: vec![A2APart::Text { text: text.into() }],
}
}
pub fn text_content(&self) -> String {
self.parts
.iter()
.filter_map(|p| match p {
A2APart::Text { text } => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct A2ATaskResponse {
pub jsonrpc: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<A2ATask>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<A2AError>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct A2ATask {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
pub status: A2ATaskStatus,
#[serde(default)]
pub history: Vec<A2AMessage>,
#[serde(default)]
pub artifacts: Vec<A2AArtifact>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct A2ATaskStatus {
pub state: TaskState,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<A2AMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<String>,
}
impl A2ATaskStatus {
pub fn new(state: TaskState) -> Self {
Self {
state,
message: None,
timestamp: Some(chrono::Utc::now().to_rfc3339()),
}
}
pub fn with_message(state: TaskState, message: A2AMessage) -> Self {
Self {
state,
message: Some(message),
timestamp: Some(chrono::Utc::now().to_rfc3339()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct A2AArtifact {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub index: Option<usize>,
pub parts: Vec<A2APart>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub append: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct A2AError {
pub code: i32,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum A2AStreamEvent {
#[serde(rename = "status")]
StatusUpdate(TaskStatusUpdateEvent),
#[serde(rename = "artifact")]
ArtifactUpdate(TaskArtifactUpdateEvent),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskStatusUpdateEvent {
pub task_id: String,
pub status: A2ATaskStatus,
#[serde(rename = "final", default)]
pub is_final: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskArtifactUpdateEvent {
pub task_id: String,
pub artifact: A2AArtifact,
#[serde(rename = "final", default)]
pub is_final: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct A2AStreamResponse {
pub jsonrpc: String,
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<A2AStreamEvent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<A2AError>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_agent_card_builder() {
let card = AgentCard::builder("test-agent", "http://localhost:8080")
.description("Test agent")
.version("1.0.0")
.skill(AgentSkill::new("calc", "Math calculation"))
.streaming()
.build();
assert_eq!(card.name, "test-agent");
assert_eq!(card.description.as_deref(), Some("Test agent"));
assert_eq!(card.skills.len(), 1);
assert!(card.capabilities.streaming);
}
#[test]
fn test_agent_card_serialization() {
let card = AgentCard::builder("test", "http://localhost")
.skill(AgentSkill::new("echo", "Echo"))
.build();
let json = serde_json::to_string_pretty(&card).unwrap();
assert!(json.contains("\"name\":"));
assert!(json.contains("\"skills\":"));
let parsed: AgentCard = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.name, "test");
}
#[test]
fn test_a2a_message() {
let msg = A2AMessage::user_text("Hello");
assert_eq!(msg.role, "user");
assert_eq!(msg.text_content(), "Hello");
}
#[test]
fn test_agent_skill() {
let skill = AgentSkill::new("translate", "Translate")
.with_tags(vec!["nlp", "translation"])
.with_examples(vec!["Translate 'hello' to Chinese"]);
assert_eq!(skill.id, "translate");
assert_eq!(skill.tags.len(), 2);
assert_eq!(skill.examples.len(), 1);
}
#[test]
fn test_task_state_terminal() {
assert!(!TaskState::Submitted.is_terminal());
assert!(!TaskState::Working.is_terminal());
assert!(!TaskState::InputRequired.is_terminal());
assert!(TaskState::Completed.is_terminal());
assert!(TaskState::Failed.is_terminal());
assert!(TaskState::Canceled.is_terminal());
}
#[test]
fn test_task_state_transitions() {
assert!(TaskState::Submitted.can_transition_to(TaskState::Working));
assert!(TaskState::Submitted.can_transition_to(TaskState::Canceled));
assert!(!TaskState::Submitted.can_transition_to(TaskState::Completed));
assert!(TaskState::Working.can_transition_to(TaskState::Completed));
assert!(TaskState::Working.can_transition_to(TaskState::Failed));
assert!(TaskState::Working.can_transition_to(TaskState::InputRequired));
assert!(TaskState::Working.can_transition_to(TaskState::Canceled));
assert!(!TaskState::Working.can_transition_to(TaskState::Submitted));
assert!(TaskState::InputRequired.can_transition_to(TaskState::Working));
assert!(TaskState::InputRequired.can_transition_to(TaskState::Canceled));
assert!(!TaskState::InputRequired.can_transition_to(TaskState::Completed));
assert!(!TaskState::Completed.can_transition_to(TaskState::Working));
assert!(!TaskState::Failed.can_transition_to(TaskState::Working));
assert!(!TaskState::Canceled.can_transition_to(TaskState::Working));
}
#[test]
fn test_task_state_serde_kebab_case() {
let json = serde_json::to_string(&TaskState::InputRequired).unwrap();
assert_eq!(json, "\"input-required\"");
let parsed: TaskState = serde_json::from_str("\"input-required\"").unwrap();
assert_eq!(parsed, TaskState::InputRequired);
let parsed: TaskState = serde_json::from_str("\"working\"").unwrap();
assert_eq!(parsed, TaskState::Working);
}
#[test]
fn test_task_status_with_timestamp() {
let status = A2ATaskStatus::new(TaskState::Working);
assert_eq!(status.state, TaskState::Working);
assert!(status.timestamp.is_some());
assert!(status.message.is_none());
let status =
A2ATaskStatus::with_message(TaskState::Completed, A2AMessage::agent_text("done"));
assert_eq!(status.state, TaskState::Completed);
assert!(status.message.is_some());
}
#[test]
fn test_artifact_with_streaming_fields() {
let artifact = A2AArtifact {
name: Some("output".to_string()),
index: Some(0),
parts: vec![A2APart::Text {
text: "chunk".into(),
}],
append: true,
};
let json = serde_json::to_string(&artifact).unwrap();
assert!(json.contains("\"index\":0"));
assert!(json.contains("\"append\":true"));
let non_append = A2AArtifact {
name: None,
index: None,
parts: vec![A2APart::Text {
text: "full".into(),
}],
append: false,
};
let json = serde_json::to_string(&non_append).unwrap();
assert!(!json.contains("index"));
assert!(!json.contains("append"));
}
#[test]
fn test_stream_event_serialization() {
let event = A2AStreamEvent::StatusUpdate(TaskStatusUpdateEvent {
task_id: "t1".into(),
status: A2ATaskStatus::new(TaskState::Working),
is_final: false,
});
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("\"type\":\"status\""));
assert!(json.contains("\"working\""));
let event = A2AStreamEvent::ArtifactUpdate(TaskArtifactUpdateEvent {
task_id: "t1".into(),
artifact: A2AArtifact {
name: None,
index: Some(0),
parts: vec![A2APart::Text { text: "hi".into() }],
append: true,
},
is_final: false,
});
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("\"type\":\"artifact\""));
}
}