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_url(mut self, url: impl Into<String>) -> Self {
109 self.url = url.into();
110 self
111 }
112
113 pub fn with_provider(mut self, provider: AgentProvider) -> Self {
115 self.provider = Some(provider);
116 self
117 }
118
119 pub fn with_capabilities(mut self, capabilities: AgentCapabilities) -> Self {
121 self.capabilities = Some(capabilities);
122 self
123 }
124
125 pub fn add_skill(mut self, skill: AgentSkill) -> Self {
127 self.skills.push(skill);
128 self
129 }
130
131 fn supports_streaming(&self) -> bool {
133 self.capabilities.as_ref().map(|c| c.streaming).unwrap_or(false)
134 }
135
136 fn supports_push_notifications(&self) -> bool {
138 self.capabilities.as_ref().map(|c| c.push_notifications).unwrap_or(false)
139 }
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct AgentProvider {
145 pub organization: String,
147 #[serde(skip_serializing_if = "Option::is_none")]
149 pub url: Option<String>,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize, Default)]
154#[serde(rename_all = "camelCase")]
155pub struct AgentCapabilities {
156 #[serde(default)]
158 pub streaming: bool,
159 #[serde(default)]
161 pub push_notifications: bool,
162 #[serde(default)]
164 pub state_transition_history: bool,
165 #[serde(skip_serializing_if = "Vec::is_empty", default)]
167 pub extensions: Vec<String>,
168}
169
170impl AgentCapabilities {
171 pub fn with_streaming() -> Self {
173 Self { streaming: true, ..Default::default() }
174 }
175
176 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#[derive(Debug, Clone, Serialize, Deserialize)]
189#[serde(rename_all = "camelCase")]
190pub struct AgentSkill {
191 id: String,
193 pub name: String,
195 #[serde(skip_serializing_if = "Option::is_none")]
197 pub description: Option<String>,
198 #[serde(skip_serializing_if = "Vec::is_empty", default)]
200 pub tags: Vec<String>,
201 #[serde(skip_serializing_if = "Vec::is_empty", default)]
203 examples: Vec<SkillExample>,
204 #[serde(skip_serializing_if = "Option::is_none")]
206 input_modes: Option<Vec<String>>,
207 #[serde(skip_serializing_if = "Option::is_none")]
209 output_modes: Option<Vec<String>>,
210}
211
212impl AgentSkill {
213 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 fn with_description(mut self, description: impl Into<String>) -> Self {
228 self.description = Some(description.into());
229 self
230 }
231
232 fn with_tags(mut self, tags: Vec<String>) -> Self {
234 self.tags = tags;
235 self
236 }
237
238 fn add_example(mut self, example: SkillExample) -> Self {
240 self.examples.push(example);
241 self
242 }
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct SkillExample {
248 input: String,
250 output: String,
252}
253
254impl SkillExample {
255 fn new(input: impl Into<String>, output: impl Into<String>) -> Self {
257 Self { input: input.into(), output: output.into() }
258 }
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct AgentCardSignature {
264 algorithm: String,
266 key_id: String,
268 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}