Skip to main content

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