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    /// Advertise bearer-token authentication for protected agent endpoints.
108    pub fn with_bearer_auth(mut self) -> Self {
109        drop(
110            self.security_schemes
111                .get_or_insert_with(HashMap::new)
112                .insert("bearerAuth".to_string(), serde_json::json!({"type": "http", "scheme": "bearer"})),
113        );
114
115        let requirements = self.security.get_or_insert_with(Vec::new);
116        if !requirements.iter().any(|requirement| requirement.contains_key("bearerAuth")) {
117            let mut requirement = HashMap::new();
118            drop(requirement.insert("bearerAuth".to_string(), Vec::new()));
119            requirements.push(requirement);
120        }
121
122        self
123    }
124
125    /// Set the URL
126    pub fn with_url(mut self, url: impl Into<String>) -> Self {
127        self.url = url.into();
128        self
129    }
130
131    /// Set the provider
132    pub fn with_provider(mut self, provider: AgentProvider) -> Self {
133        self.provider = Some(provider);
134        self
135    }
136
137    /// Set the capabilities
138    pub fn with_capabilities(mut self, capabilities: AgentCapabilities) -> Self {
139        self.capabilities = Some(capabilities);
140        self
141    }
142
143    /// Add a skill
144    pub fn add_skill(mut self, skill: AgentSkill) -> Self {
145        self.skills.push(skill);
146        self
147    }
148
149    /// Check if streaming is supported
150    fn supports_streaming(&self) -> bool {
151        self.capabilities.as_ref().map(|c| c.streaming).unwrap_or(false)
152    }
153
154    /// Check if push notifications are supported
155    fn supports_push_notifications(&self) -> bool {
156        self.capabilities.as_ref().map(|c| c.push_notifications).unwrap_or(false)
157    }
158}
159
160/// Agent provider/organization details
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct AgentProvider {
163    /// Organization name
164    pub organization: String,
165    /// Organization URL
166    #[serde(skip_serializing_if = "Option::is_none")]
167    pub url: Option<String>,
168}
169
170/// Agent capabilities declaration
171#[derive(Debug, Clone, Serialize, Deserialize, Default)]
172#[serde(rename_all = "camelCase")]
173pub struct AgentCapabilities {
174    /// Whether streaming via SSE is supported
175    #[serde(default)]
176    pub streaming: bool,
177    /// Whether push notifications are supported
178    #[serde(default)]
179    pub push_notifications: bool,
180    /// Whether state transition history is maintained
181    #[serde(default)]
182    pub state_transition_history: bool,
183    /// List of supported extensions
184    #[serde(skip_serializing_if = "Vec::is_empty", default)]
185    pub extensions: Vec<String>,
186}
187
188impl AgentCapabilities {
189    /// Create capabilities with streaming enabled
190    pub fn with_streaming() -> Self {
191        Self { streaming: true, ..Default::default() }
192    }
193
194    /// Create capabilities with all features enabled
195    fn full() -> Self {
196        Self {
197            streaming: true,
198            push_notifications: true,
199            state_transition_history: true,
200            extensions: Vec::new(),
201        }
202    }
203}
204
205/// A specific capability/skill of an agent
206#[derive(Debug, Clone, Serialize, Deserialize)]
207#[serde(rename_all = "camelCase")]
208pub struct AgentSkill {
209    /// Unique skill identifier
210    id: String,
211    /// Human-readable skill name
212    pub name: String,
213    /// Skill description
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub description: Option<String>,
216    /// Tags for categorization
217    #[serde(skip_serializing_if = "Vec::is_empty", default)]
218    pub tags: Vec<String>,
219    /// Example inputs/outputs
220    #[serde(skip_serializing_if = "Vec::is_empty", default)]
221    examples: Vec<SkillExample>,
222    /// Input modes specific to this skill
223    #[serde(skip_serializing_if = "Option::is_none")]
224    input_modes: Option<Vec<String>>,
225    /// Output modes specific to this skill
226    #[serde(skip_serializing_if = "Option::is_none")]
227    output_modes: Option<Vec<String>>,
228}
229
230impl AgentSkill {
231    /// Create a new skill
232    fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
233        Self {
234            id: id.into(),
235            name: name.into(),
236            description: None,
237            tags: Vec::new(),
238            examples: Vec::new(),
239            input_modes: None,
240            output_modes: None,
241        }
242    }
243
244    /// Add a description
245    fn with_description(mut self, description: impl Into<String>) -> Self {
246        self.description = Some(description.into());
247        self
248    }
249
250    /// Add tags
251    fn with_tags(mut self, tags: Vec<String>) -> Self {
252        self.tags = tags;
253        self
254    }
255
256    /// Add an example
257    fn add_example(mut self, example: SkillExample) -> Self {
258        self.examples.push(example);
259        self
260    }
261}
262
263/// Example input/output for a skill
264#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct SkillExample {
266    /// Example input
267    input: String,
268    /// Example output
269    output: String,
270}
271
272impl SkillExample {
273    /// Create a new example
274    fn new(input: impl Into<String>, output: impl Into<String>) -> Self {
275        Self { input: input.into(), output: output.into() }
276    }
277}
278
279/// Agent Card signature for verification
280#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct AgentCardSignature {
282    /// Algorithm used
283    algorithm: String,
284    /// Key ID
285    key_id: String,
286    /// The signature value
287    signature: String,
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn test_agent_card_creation() {
296        let card = AgentCard::new("test-agent", "A test agent", "1.0.0");
297        assert_eq!(card.name, "test-agent");
298        assert_eq!(card.protocol_version, A2A_PROTOCOL_VERSION);
299    }
300
301    #[test]
302    fn test_vtcode_default_card() {
303        let card = AgentCard::vtcode_default("http://localhost:8080");
304        assert_eq!(card.name, "vtcode-agent");
305        assert_eq!(card.url, "http://localhost:8080");
306        assert!(card.supports_streaming());
307        assert!(!card.supports_push_notifications());
308    }
309
310    #[test]
311    fn test_agent_card_serialization() {
312        let card = AgentCard::vtcode_default("http://localhost:8080");
313        let json = serde_json::to_string_pretty(&card).expect("serialize");
314        assert!(json.contains("\"protocolVersion\""));
315        assert!(json.contains("vtcode-agent"));
316    }
317
318    #[test]
319    fn test_agent_skill() {
320        let skill = AgentSkill::new("code-gen", "Code Generation")
321            .with_description("Generate code from natural language")
322            .with_tags(vec!["coding".to_string(), "generation".to_string()])
323            .add_example(SkillExample::new(
324                "Create a Python function to sort a list",
325                "def sort_list(items): return sorted(items)",
326            ));
327
328        assert_eq!(skill.id, "code-gen");
329        assert_eq!(skill.tags.len(), 2);
330        assert_eq!(skill.examples.len(), 1);
331    }
332
333    #[test]
334    fn test_capabilities() {
335        let caps = AgentCapabilities::full();
336        assert!(caps.streaming);
337        assert!(caps.push_notifications);
338        assert!(caps.state_transition_history);
339    }
340}