1use hashbrown::HashMap;
7use serde::{Deserialize, Serialize};
8
9const A2A_PROTOCOL_VERSION: &str = "1.0";
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct AgentCard {
20 pub protocol_version: String,
22 pub name: String,
24 pub description: String,
26 pub version: String,
28 pub url: String,
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub provider: Option<AgentProvider>,
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub capabilities: Option<AgentCapabilities>,
36 #[serde(skip_serializing_if = "Vec::is_empty", default)]
38 pub default_input_modes: Vec<String>,
39 #[serde(skip_serializing_if = "Vec::is_empty", default)]
41 pub default_output_modes: Vec<String>,
42 #[serde(skip_serializing_if = "Vec::is_empty", default)]
44 pub skills: Vec<AgentSkill>,
45 #[serde(skip_serializing_if = "Option::is_none")]
47 security_schemes: Option<HashMap<String, serde_json::Value>>,
48 #[serde(skip_serializing_if = "Option::is_none")]
50 security: Option<Vec<HashMap<String, Vec<String>>>>,
51 #[serde(skip_serializing_if = "Option::is_none")]
53 supports_authenticated_extended_card: Option<bool>,
54 #[serde(skip_serializing_if = "Option::is_none")]
56 signatures: Option<Vec<AgentCardSignature>>,
57}
58
59impl AgentCard {
60 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 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 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 pub fn with_url(mut self, url: impl Into<String>) -> Self {
127 self.url = url.into();
128 self
129 }
130
131 pub fn with_provider(mut self, provider: AgentProvider) -> Self {
133 self.provider = Some(provider);
134 self
135 }
136
137 pub fn with_capabilities(mut self, capabilities: AgentCapabilities) -> Self {
139 self.capabilities = Some(capabilities);
140 self
141 }
142
143 pub fn add_skill(mut self, skill: AgentSkill) -> Self {
145 self.skills.push(skill);
146 self
147 }
148
149 fn supports_streaming(&self) -> bool {
151 self.capabilities.as_ref().map(|c| c.streaming).unwrap_or(false)
152 }
153
154 fn supports_push_notifications(&self) -> bool {
156 self.capabilities.as_ref().map(|c| c.push_notifications).unwrap_or(false)
157 }
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct AgentProvider {
163 pub organization: String,
165 #[serde(skip_serializing_if = "Option::is_none")]
167 pub url: Option<String>,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize, Default)]
172#[serde(rename_all = "camelCase")]
173pub struct AgentCapabilities {
174 #[serde(default)]
176 pub streaming: bool,
177 #[serde(default)]
179 pub push_notifications: bool,
180 #[serde(default)]
182 pub state_transition_history: bool,
183 #[serde(skip_serializing_if = "Vec::is_empty", default)]
185 pub extensions: Vec<String>,
186}
187
188impl AgentCapabilities {
189 pub fn with_streaming() -> Self {
191 Self { streaming: true, ..Default::default() }
192 }
193
194 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#[derive(Debug, Clone, Serialize, Deserialize)]
207#[serde(rename_all = "camelCase")]
208pub struct AgentSkill {
209 id: String,
211 pub name: String,
213 #[serde(skip_serializing_if = "Option::is_none")]
215 pub description: Option<String>,
216 #[serde(skip_serializing_if = "Vec::is_empty", default)]
218 pub tags: Vec<String>,
219 #[serde(skip_serializing_if = "Vec::is_empty", default)]
221 examples: Vec<SkillExample>,
222 #[serde(skip_serializing_if = "Option::is_none")]
224 input_modes: Option<Vec<String>>,
225 #[serde(skip_serializing_if = "Option::is_none")]
227 output_modes: Option<Vec<String>>,
228}
229
230impl AgentSkill {
231 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 fn with_description(mut self, description: impl Into<String>) -> Self {
246 self.description = Some(description.into());
247 self
248 }
249
250 fn with_tags(mut self, tags: Vec<String>) -> Self {
252 self.tags = tags;
253 self
254 }
255
256 fn add_example(mut self, example: SkillExample) -> Self {
258 self.examples.push(example);
259 self
260 }
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct SkillExample {
266 input: String,
268 output: String,
270}
271
272impl SkillExample {
273 fn new(input: impl Into<String>, output: impl Into<String>) -> Self {
275 Self { input: input.into(), output: output.into() }
276 }
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct AgentCardSignature {
282 algorithm: String,
284 key_id: String,
286 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}