Skip to main content

vtcode_a2a/
agent_card.rs

1//! Agent Card for A2A Protocol
2//!
3//! Implements the Agent Card structure used for agent discovery and capability
4//! advertisement, typically served at `/.well-known/agent-card.json`.
5
6use hashbrown::HashMap;
7use serde::{Deserialize, Serialize};
8
9/// A2A Protocol version
10const A2A_PROTOCOL_VERSION: &str = "1.0";
11
12/// Agent Card - metadata describing an A2A agent
13///
14/// Agent Cards are used for agent discovery. They are typically served at
15/// `/.well-known/agent-card.json` and describe the agent's identity, capabilities,
16/// and security requirements.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct AgentCard {
20    /// The version of the A2A protocol supported
21    pub protocol_version: String,
22    /// Agent name
23    pub name: String,
24    /// Agent description
25    pub description: String,
26    /// Agent version
27    pub version: String,
28    /// The preferred endpoint URL for the agent's A2A service
29    pub url: String,
30    /// Organization/provider details
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub provider: Option<AgentProvider>,
33    /// Features supported by this agent
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub capabilities: Option<AgentCapabilities>,
36    /// Default supported input MIME types
37    #[serde(skip_serializing_if = "Vec::is_empty", default)]
38    pub default_input_modes: Vec<String>,
39    /// Default supported output MIME types
40    #[serde(skip_serializing_if = "Vec::is_empty", default)]
41    pub default_output_modes: Vec<String>,
42    /// List of specific skills/capabilities
43    #[serde(skip_serializing_if = "Vec::is_empty", default)]
44    pub skills: Vec<AgentSkill>,
45    /// Security schemes following OpenAPI specification
46    #[serde(skip_serializing_if = "Option::is_none")]
47    security_schemes: Option<HashMap<String, serde_json::Value>>,
48    /// Security requirements
49    #[serde(skip_serializing_if = "Option::is_none")]
50    security: Option<Vec<HashMap<String, Vec<String>>>>,
51    /// Whether a more detailed card is available post-authentication
52    #[serde(skip_serializing_if = "Option::is_none")]
53    supports_authenticated_extended_card: Option<bool>,
54    /// JWS signatures for card verification
55    #[serde(skip_serializing_if = "Option::is_none")]
56    signatures: Option<Vec<AgentCardSignature>>,
57}
58
59impl AgentCard {
60    /// Create a new Agent Card with required fields
61    fn new(name: impl Into<String>, description: impl Into<String>, version: impl Into<String>) -> Self {
62        Self {
63            protocol_version: A2A_PROTOCOL_VERSION.to_string(),
64            name: name.into(),
65            description: description.into(),
66            version: version.into(),
67            url: String::new(),
68            provider: None,
69            capabilities: None,
70            default_input_modes: vec!["text/plain".to_string()],
71            default_output_modes: vec!["text/plain".to_string()],
72            skills: Vec::new(),
73            security_schemes: None,
74            security: None,
75            supports_authenticated_extended_card: None,
76            signatures: None,
77        }
78    }
79
80    /// Create a default VT Code agent card
81    pub fn vtcode_default(url: impl Into<String>) -> Self {
82        let mut card = Self::new(
83            "vtcode-agent",
84            "VT Code AI coding agent - a terminal-based coding assistant supporting multiple LLM providers",
85            env!("CARGO_PKG_VERSION"),
86        );
87        card.url = url.into();
88        card.provider = Some(AgentProvider {
89            organization: "VT Code".to_string(),
90            url: Some("https://github.com/vinhnx/vtcode".to_string()),
91        });
92        card.capabilities = Some(AgentCapabilities {
93            streaming: true,
94            push_notifications: false,
95            state_transition_history: true,
96            extensions: Vec::new(),
97        });
98        card.default_input_modes = vec!["text/plain".to_string(), "application/json".to_string()];
99        card.default_output_modes = vec![
100            "text/plain".to_string(),
101            "application/json".to_string(),
102            "text/markdown".to_string(),
103        ];
104        card
105    }
106
107    /// Set the URL
108    pub fn with_url(mut self, url: impl Into<String>) -> Self {
109        self.url = url.into();
110        self
111    }
112
113    /// Set the provider
114    pub fn with_provider(mut self, provider: AgentProvider) -> Self {
115        self.provider = Some(provider);
116        self
117    }
118
119    /// Set the capabilities
120    pub fn with_capabilities(mut self, capabilities: AgentCapabilities) -> Self {
121        self.capabilities = Some(capabilities);
122        self
123    }
124
125    /// Add a skill
126    pub fn add_skill(mut self, skill: AgentSkill) -> Self {
127        self.skills.push(skill);
128        self
129    }
130
131    /// Check if streaming is supported
132    fn supports_streaming(&self) -> bool {
133        self.capabilities.as_ref().map(|c| c.streaming).unwrap_or(false)
134    }
135
136    /// Check if push notifications are supported
137    fn supports_push_notifications(&self) -> bool {
138        self.capabilities.as_ref().map(|c| c.push_notifications).unwrap_or(false)
139    }
140}
141
142/// Agent provider/organization details
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct AgentProvider {
145    /// Organization name
146    pub organization: String,
147    /// Organization URL
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub url: Option<String>,
150}
151
152/// Agent capabilities declaration
153#[derive(Debug, Clone, Serialize, Deserialize, Default)]
154#[serde(rename_all = "camelCase")]
155pub struct AgentCapabilities {
156    /// Whether streaming via SSE is supported
157    #[serde(default)]
158    pub streaming: bool,
159    /// Whether push notifications are supported
160    #[serde(default)]
161    pub push_notifications: bool,
162    /// Whether state transition history is maintained
163    #[serde(default)]
164    pub state_transition_history: bool,
165    /// List of supported extensions
166    #[serde(skip_serializing_if = "Vec::is_empty", default)]
167    pub extensions: Vec<String>,
168}
169
170impl AgentCapabilities {
171    /// Create capabilities with streaming enabled
172    pub fn with_streaming() -> Self {
173        Self { streaming: true, ..Default::default() }
174    }
175
176    /// Create capabilities with all features enabled
177    fn full() -> Self {
178        Self {
179            streaming: true,
180            push_notifications: true,
181            state_transition_history: true,
182            extensions: Vec::new(),
183        }
184    }
185}
186
187/// A specific capability/skill of an agent
188#[derive(Debug, Clone, Serialize, Deserialize)]
189#[serde(rename_all = "camelCase")]
190pub struct AgentSkill {
191    /// Unique skill identifier
192    id: String,
193    /// Human-readable skill name
194    pub name: String,
195    /// Skill description
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub description: Option<String>,
198    /// Tags for categorization
199    #[serde(skip_serializing_if = "Vec::is_empty", default)]
200    pub tags: Vec<String>,
201    /// Example inputs/outputs
202    #[serde(skip_serializing_if = "Vec::is_empty", default)]
203    examples: Vec<SkillExample>,
204    /// Input modes specific to this skill
205    #[serde(skip_serializing_if = "Option::is_none")]
206    input_modes: Option<Vec<String>>,
207    /// Output modes specific to this skill
208    #[serde(skip_serializing_if = "Option::is_none")]
209    output_modes: Option<Vec<String>>,
210}
211
212impl AgentSkill {
213    /// Create a new skill
214    fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
215        Self {
216            id: id.into(),
217            name: name.into(),
218            description: None,
219            tags: Vec::new(),
220            examples: Vec::new(),
221            input_modes: None,
222            output_modes: None,
223        }
224    }
225
226    /// Add a description
227    fn with_description(mut self, description: impl Into<String>) -> Self {
228        self.description = Some(description.into());
229        self
230    }
231
232    /// Add tags
233    fn with_tags(mut self, tags: Vec<String>) -> Self {
234        self.tags = tags;
235        self
236    }
237
238    /// Add an example
239    fn add_example(mut self, example: SkillExample) -> Self {
240        self.examples.push(example);
241        self
242    }
243}
244
245/// Example input/output for a skill
246#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct SkillExample {
248    /// Example input
249    input: String,
250    /// Example output
251    output: String,
252}
253
254impl SkillExample {
255    /// Create a new example
256    fn new(input: impl Into<String>, output: impl Into<String>) -> Self {
257        Self { input: input.into(), output: output.into() }
258    }
259}
260
261/// Agent Card signature for verification
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct AgentCardSignature {
264    /// Algorithm used
265    algorithm: String,
266    /// Key ID
267    key_id: String,
268    /// The signature value
269    signature: String,
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    #[test]
277    fn test_agent_card_creation() {
278        let card = AgentCard::new("test-agent", "A test agent", "1.0.0");
279        assert_eq!(card.name, "test-agent");
280        assert_eq!(card.protocol_version, A2A_PROTOCOL_VERSION);
281    }
282
283    #[test]
284    fn test_vtcode_default_card() {
285        let card = AgentCard::vtcode_default("http://localhost:8080");
286        assert_eq!(card.name, "vtcode-agent");
287        assert_eq!(card.url, "http://localhost:8080");
288        assert!(card.supports_streaming());
289        assert!(!card.supports_push_notifications());
290    }
291
292    #[test]
293    fn test_agent_card_serialization() {
294        let card = AgentCard::vtcode_default("http://localhost:8080");
295        let json = serde_json::to_string_pretty(&card).expect("serialize");
296        assert!(json.contains("\"protocolVersion\""));
297        assert!(json.contains("vtcode-agent"));
298    }
299
300    #[test]
301    fn test_agent_skill() {
302        let skill = AgentSkill::new("code-gen", "Code Generation")
303            .with_description("Generate code from natural language")
304            .with_tags(vec!["coding".to_string(), "generation".to_string()])
305            .add_example(SkillExample::new(
306                "Create a Python function to sort a list",
307                "def sort_list(items): return sorted(items)",
308            ));
309
310        assert_eq!(skill.id, "code-gen");
311        assert_eq!(skill.tags.len(), 2);
312        assert_eq!(skill.examples.len(), 1);
313    }
314
315    #[test]
316    fn test_capabilities() {
317        let caps = AgentCapabilities::full();
318        assert!(caps.streaming);
319        assert!(caps.push_notifications);
320        assert!(caps.state_transition_history);
321    }
322}