1use hashbrown::HashMap;
7use serde::{Deserialize, Serialize};
8
9pub const 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 pub security_schemes: Option<HashMap<String, serde_json::Value>>,
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub security: Option<Vec<HashMap<String, Vec<String>>>>,
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub supports_authenticated_extended_card: Option<bool>,
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub signatures: Option<Vec<AgentCardSignature>>,
57}
58
59impl AgentCard {
60 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 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 pub fn with_url(mut self, url: impl Into<String>) -> Self {
113 self.url = url.into();
114 self
115 }
116
117 pub fn with_provider(mut self, provider: AgentProvider) -> Self {
119 self.provider = Some(provider);
120 self
121 }
122
123 pub fn with_capabilities(mut self, capabilities: AgentCapabilities) -> Self {
125 self.capabilities = Some(capabilities);
126 self
127 }
128
129 pub fn add_skill(mut self, skill: AgentSkill) -> Self {
131 self.skills.push(skill);
132 self
133 }
134
135 pub fn supports_streaming(&self) -> bool {
137 self.capabilities
138 .as_ref()
139 .map(|c| c.streaming)
140 .unwrap_or(false)
141 }
142
143 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#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct AgentProvider {
155 pub organization: String,
157 #[serde(skip_serializing_if = "Option::is_none")]
159 pub url: Option<String>,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize, Default)]
164#[serde(rename_all = "camelCase")]
165pub struct AgentCapabilities {
166 #[serde(default)]
168 pub streaming: bool,
169 #[serde(default)]
171 pub push_notifications: bool,
172 #[serde(default)]
174 pub state_transition_history: bool,
175 #[serde(skip_serializing_if = "Vec::is_empty", default)]
177 pub extensions: Vec<String>,
178}
179
180impl AgentCapabilities {
181 pub fn with_streaming() -> Self {
183 Self {
184 streaming: true,
185 ..Default::default()
186 }
187 }
188
189 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#[derive(Debug, Clone, Serialize, Deserialize)]
202#[serde(rename_all = "camelCase")]
203pub struct AgentSkill {
204 pub id: String,
206 pub name: String,
208 #[serde(skip_serializing_if = "Option::is_none")]
210 pub description: Option<String>,
211 #[serde(skip_serializing_if = "Vec::is_empty", default)]
213 pub tags: Vec<String>,
214 #[serde(skip_serializing_if = "Vec::is_empty", default)]
216 pub examples: Vec<SkillExample>,
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub input_modes: Option<Vec<String>>,
220 #[serde(skip_serializing_if = "Option::is_none")]
222 pub output_modes: Option<Vec<String>>,
223}
224
225impl AgentSkill {
226 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 pub fn with_description(mut self, description: impl Into<String>) -> Self {
241 self.description = Some(description.into());
242 self
243 }
244
245 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
247 self.tags = tags;
248 self
249 }
250
251 pub fn add_example(mut self, example: SkillExample) -> Self {
253 self.examples.push(example);
254 self
255 }
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct SkillExample {
261 pub input: String,
263 pub output: String,
265}
266
267impl SkillExample {
268 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#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct AgentCardSignature {
280 pub algorithm: String,
282 pub key_id: String,
284 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}