use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentCard {
pub name: String,
pub description: String,
pub version: String,
pub url: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub capabilities: Vec<AgentCapability>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authentication: Option<AgentAuth>,
#[serde(default)]
pub protocol_version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentCapability {
#[serde(rename = "type")]
pub capability_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentAuth {
#[serde(rename = "type")]
pub auth_type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum TaskState {
Submitted,
Working,
InputRequired,
Completed,
Failed,
Canceled,
}
#[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<A2aMessage>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct A2aTask {
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<A2aMessage>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum A2aRole {
User,
Agent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct A2aMessage {
pub role: A2aRole,
pub parts: Vec<Part>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message_id: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "camelCase")]
pub enum Part {
#[serde(rename = "text")]
Text {
text: String,
},
#[serde(rename = "data")]
Data {
data: serde_json::Value,
},
#[serde(rename = "file")]
File {
file: FileContent,
},
}
#[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)]
#[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 = "HashMap::is_empty")]
pub metadata: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcRequest {
pub jsonrpc: String,
pub id: serde_json::Value,
pub method: String,
#[serde(default)]
pub params: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcResponse {
pub jsonrpc: String,
pub id: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<JsonRpcError>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcError {
pub code: i64,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
}
impl JsonRpcResponse {
pub fn success(id: serde_json::Value, result: serde_json::Value) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id,
result: Some(result),
error: None,
}
}
pub fn error(id: serde_json::Value, code: i64, message: impl Into<String>) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id,
result: None,
error: Some(JsonRpcError {
code,
message: message.into(),
data: None,
}),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskSendParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub message: A2aMessage,
#[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, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskGetParams {
pub id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskCancelParams {
pub id: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_agent_card_roundtrip() {
let card = AgentCard {
name: "TestAgent".to_string(),
description: "A test agent".to_string(),
version: "1.0.0".to_string(),
url: "https://example.com/a2a".to_string(),
capabilities: vec![AgentCapability {
capability_type: "streaming".to_string(),
description: Some("Supports SSE streaming".to_string()),
}],
authentication: Some(AgentAuth {
auth_type: "api_key".to_string(),
instructions: Some("Set X-API-Key header".to_string()),
}),
protocol_version: "0.2".to_string(),
};
let json = serde_json::to_string(&card).unwrap();
let parsed: AgentCard = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.name, "TestAgent");
assert_eq!(parsed.capabilities.len(), 1);
}
#[test]
fn test_task_roundtrip() {
let task = A2aTask {
id: "task-123".to_string(),
context_id: Some("ctx-1".to_string()),
status: TaskStatus {
state: TaskState::Completed,
message: None,
},
artifacts: vec![Artifact {
artifact_id: "art-1".to_string(),
name: Some("result.txt".to_string()),
parts: vec![Part::Text {
text: "Hello world".to_string(),
}],
metadata: HashMap::new(),
}],
history: vec![A2aMessage {
role: A2aRole::User,
parts: vec![Part::Text {
text: "Do something".to_string(),
}],
message_id: Some("msg-1".to_string()),
metadata: HashMap::new(),
}],
metadata: HashMap::new(),
};
let json = serde_json::to_string_pretty(&task).unwrap();
let parsed: A2aTask = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.id, "task-123");
assert_eq!(parsed.status.state, TaskState::Completed);
assert_eq!(parsed.artifacts.len(), 1);
}
#[test]
fn test_json_rpc_response() {
let resp = JsonRpcResponse::success(
serde_json::json!(1),
serde_json::json!({"status": "ok"}),
);
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("\"jsonrpc\":\"2.0\""));
assert!(json.contains("\"result\""));
let err_resp = JsonRpcResponse::error(
serde_json::json!(2),
-32600,
"Invalid request",
);
let err_json = serde_json::to_string(&err_resp).unwrap();
assert!(err_json.contains("\"error\""));
}
#[test]
fn test_part_variants() {
let text = Part::Text {
text: "hello".to_string(),
};
let json = serde_json::to_string(&text).unwrap();
assert!(json.contains("\"kind\":\"text\""));
let data = Part::Data {
data: serde_json::json!({"key": "value"}),
};
let json = serde_json::to_string(&data).unwrap();
assert!(json.contains("\"kind\":\"data\""));
}
}