use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
pub const A2A_PROTOCOL_VERSION: &str = "1.0";
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentCard {
pub protocol_version: String,
pub name: String,
pub description: String,
pub version: String,
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub provider: Option<AgentProvider>,
#[serde(skip_serializing_if = "Option::is_none")]
pub capabilities: Option<AgentCapabilities>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub default_input_modes: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub default_output_modes: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub skills: Vec<AgentSkill>,
#[serde(skip_serializing_if = "Option::is_none")]
pub security_schemes: Option<HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub security: Option<Vec<HashMap<String, Vec<String>>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supports_authenticated_extended_card: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signatures: Option<Vec<AgentCardSignature>>,
}
impl AgentCard {
pub fn new(
name: impl Into<String>,
description: impl Into<String>,
version: impl Into<String>,
) -> Self {
Self {
protocol_version: A2A_PROTOCOL_VERSION.to_string(),
name: name.into(),
description: description.into(),
version: version.into(),
url: String::new(),
provider: None,
capabilities: None,
default_input_modes: vec!["text/plain".to_string()],
default_output_modes: vec!["text/plain".to_string()],
skills: Vec::new(),
security_schemes: None,
security: None,
supports_authenticated_extended_card: None,
signatures: None,
}
}
pub fn vtcode_default(url: impl Into<String>) -> Self {
let mut card = Self::new(
"vtcode-agent",
"VT Code AI coding agent - a terminal-based coding assistant supporting multiple LLM providers",
env!("CARGO_PKG_VERSION"),
);
card.url = url.into();
card.provider = Some(AgentProvider {
organization: "VT Code".to_string(),
url: Some("https://github.com/vinhnx/vtcode".to_string()),
});
card.capabilities = Some(AgentCapabilities {
streaming: true,
push_notifications: false,
state_transition_history: true,
extensions: Vec::new(),
});
card.default_input_modes = vec!["text/plain".to_string(), "application/json".to_string()];
card.default_output_modes = vec![
"text/plain".to_string(),
"application/json".to_string(),
"text/markdown".to_string(),
];
card
}
pub fn with_url(mut self, url: impl Into<String>) -> Self {
self.url = url.into();
self
}
pub fn with_provider(mut self, provider: AgentProvider) -> Self {
self.provider = Some(provider);
self
}
pub fn with_capabilities(mut self, capabilities: AgentCapabilities) -> Self {
self.capabilities = Some(capabilities);
self
}
pub fn add_skill(mut self, skill: AgentSkill) -> Self {
self.skills.push(skill);
self
}
pub fn supports_streaming(&self) -> bool {
self.capabilities
.as_ref()
.map(|c| c.streaming)
.unwrap_or(false)
}
pub fn supports_push_notifications(&self) -> bool {
self.capabilities
.as_ref()
.map(|c| c.push_notifications)
.unwrap_or(false)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentProvider {
pub organization: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[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,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub extensions: Vec<String>,
}
impl AgentCapabilities {
pub fn with_streaming() -> Self {
Self {
streaming: true,
..Default::default()
}
}
pub fn full() -> Self {
Self {
streaming: true,
push_notifications: true,
state_transition_history: true,
extensions: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentSkill {
pub id: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub tags: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub examples: Vec<SkillExample>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_modes: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_modes: Option<Vec<String>>,
}
impl AgentSkill {
pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
Self {
id: id.into(),
name: name.into(),
description: None,
tags: Vec::new(),
examples: Vec::new(),
input_modes: None,
output_modes: None,
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_tags(mut self, tags: Vec<String>) -> Self {
self.tags = tags;
self
}
pub fn add_example(mut self, example: SkillExample) -> Self {
self.examples.push(example);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillExample {
pub input: String,
pub output: String,
}
impl SkillExample {
pub fn new(input: impl Into<String>, output: impl Into<String>) -> Self {
Self {
input: input.into(),
output: output.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentCardSignature {
pub algorithm: String,
pub key_id: String,
pub signature: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_agent_card_creation() {
let card = AgentCard::new("test-agent", "A test agent", "1.0.0");
assert_eq!(card.name, "test-agent");
assert_eq!(card.protocol_version, A2A_PROTOCOL_VERSION);
}
#[test]
fn test_vtcode_default_card() {
let card = AgentCard::vtcode_default("http://localhost:8080");
assert_eq!(card.name, "vtcode-agent");
assert_eq!(card.url, "http://localhost:8080");
assert!(card.supports_streaming());
assert!(!card.supports_push_notifications());
}
#[test]
fn test_agent_card_serialization() {
let card = AgentCard::vtcode_default("http://localhost:8080");
let json = serde_json::to_string_pretty(&card).expect("serialize");
assert!(json.contains("\"protocolVersion\""));
assert!(json.contains("vtcode-agent"));
}
#[test]
fn test_agent_skill() {
let skill = AgentSkill::new("code-gen", "Code Generation")
.with_description("Generate code from natural language")
.with_tags(vec!["coding".to_string(), "generation".to_string()])
.add_example(SkillExample::new(
"Create a Python function to sort a list",
"def sort_list(items): return sorted(items)",
));
assert_eq!(skill.id, "code-gen");
assert_eq!(skill.tags.len(), 2);
assert_eq!(skill.examples.len(), 1);
}
#[test]
fn test_capabilities() {
let caps = AgentCapabilities::full();
assert!(caps.streaming);
assert!(caps.push_notifications);
assert!(caps.state_transition_history);
}
}