1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
6#[serde(rename_all = "lowercase")]
7pub enum Role {
8 User,
9 Agent,
10}
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14#[serde(rename_all = "camelCase")]
15pub struct FileContent {
16 #[serde(skip_serializing_if = "Option::is_none")]
17 pub name: Option<String>,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub mime_type: Option<String>,
20 #[serde(skip_serializing_if = "Option::is_none")]
21 pub bytes: Option<String>,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub url: Option<String>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
28#[serde(tag = "kind", rename_all = "lowercase")]
29pub enum Part {
30 #[serde(rename_all = "camelCase")]
31 Text {
32 text: String,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 metadata: Option<HashMap<String, serde_json::Value>>,
35 },
36 #[serde(rename_all = "camelCase")]
37 File {
38 file: FileContent,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 metadata: Option<HashMap<String, serde_json::Value>>,
41 },
42 #[serde(rename_all = "camelCase")]
43 Data {
44 data: serde_json::Value,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 metadata: Option<HashMap<String, serde_json::Value>>,
47 },
48}
49
50#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
52#[serde(rename_all = "kebab-case")]
53pub enum TaskState {
54 Unknown,
55 Submitted,
56 Working,
57 InputRequired,
58 AuthRequired,
59 Completed,
60 Failed,
61 Canceled,
62 Rejected,
63}
64
65impl std::fmt::Display for TaskState {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 let s = match self {
68 Self::Unknown => "unknown",
69 Self::Submitted => "submitted",
70 Self::Working => "working",
71 Self::InputRequired => "input-required",
72 Self::AuthRequired => "auth-required",
73 Self::Completed => "completed",
74 Self::Failed => "failed",
75 Self::Canceled => "canceled",
76 Self::Rejected => "rejected",
77 };
78 write!(f, "{s}")
79 }
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
84#[serde(rename_all = "camelCase")]
85pub struct TaskStatus {
86 pub state: TaskState,
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub message: Option<Message>,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub timestamp: Option<String>,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
95#[serde(rename_all = "camelCase")]
96pub struct Message {
97 pub message_id: String,
98 pub role: Role,
99 pub parts: Vec<Part>,
100 #[serde(skip_serializing_if = "Option::is_none")]
101 pub context_id: Option<String>,
102 #[serde(skip_serializing_if = "Option::is_none")]
103 pub task_id: Option<String>,
104 #[serde(skip_serializing_if = "Option::is_none")]
105 pub metadata: Option<HashMap<String, serde_json::Value>>,
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub reference_task_ids: Option<Vec<String>>,
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub extensions: Option<Vec<String>>,
110 #[serde(default = "message_kind")]
111 pub kind: String,
112}
113
114fn message_kind() -> String {
115 "message".to_string()
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
120#[serde(rename_all = "camelCase")]
121pub struct Artifact {
122 pub artifact_id: String,
123 #[serde(skip_serializing_if = "Option::is_none")]
124 pub name: Option<String>,
125 #[serde(skip_serializing_if = "Option::is_none")]
126 pub description: Option<String>,
127 pub parts: Vec<Part>,
128 #[serde(skip_serializing_if = "Option::is_none")]
129 pub metadata: Option<HashMap<String, serde_json::Value>>,
130 #[serde(skip_serializing_if = "Option::is_none")]
131 pub extensions: Option<Vec<String>>,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
136#[serde(rename_all = "camelCase")]
137pub struct Task {
138 pub id: String,
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub context_id: Option<String>,
141 pub status: TaskStatus,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub artifacts: Option<Vec<Artifact>>,
144 #[serde(skip_serializing_if = "Option::is_none")]
145 pub history: Option<Vec<Message>>,
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub metadata: Option<HashMap<String, serde_json::Value>>,
148 #[serde(default = "task_kind")]
149 pub kind: String,
150}
151
152fn task_kind() -> String {
153 "task".to_string()
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
158#[serde(rename_all = "camelCase")]
159pub struct AgentCapabilities {
160 #[serde(skip_serializing_if = "Option::is_none")]
161 pub streaming: Option<bool>,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 pub push_notifications: Option<bool>,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
168#[serde(rename_all = "camelCase")]
169pub struct AgentProvider {
170 pub organization: String,
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub url: Option<String>,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
177#[serde(rename_all = "camelCase")]
178pub struct AgentSkill {
179 pub id: String,
180 pub name: String,
181 pub description: String,
182 pub tags: Vec<String>,
183 #[serde(skip_serializing_if = "Option::is_none")]
184 pub examples: Option<Vec<String>>,
185 #[serde(skip_serializing_if = "Option::is_none")]
186 pub input_modes: Option<Vec<String>>,
187 #[serde(skip_serializing_if = "Option::is_none")]
188 pub output_modes: Option<Vec<String>>,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
193#[serde(tag = "type", rename_all = "camelCase")]
194pub enum SecurityScheme {
195 #[serde(rename = "apiKey")]
196 ApiKey {
197 name: String,
198 #[serde(rename = "in")]
199 location: String,
200 },
201 #[serde(rename = "http")]
202 Http {
203 scheme: String,
204 #[serde(skip_serializing_if = "Option::is_none")]
205 bearer_format: Option<String>,
206 },
207 #[serde(rename = "oauth2")]
208 OAuth2 { flows: serde_json::Value },
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
213#[serde(rename_all = "camelCase")]
214pub struct AgentCard {
215 pub name: String,
216 pub description: String,
217 pub version: String,
218 pub url: String,
219 #[serde(skip_serializing_if = "Option::is_none")]
220 pub provider: Option<AgentProvider>,
221 pub capabilities: AgentCapabilities,
222 #[serde(skip_serializing_if = "Option::is_none")]
223 pub security_schemes: Option<HashMap<String, SecurityScheme>>,
224 #[serde(skip_serializing_if = "Option::is_none")]
225 pub security_requirements: Option<Vec<HashMap<String, Vec<String>>>>,
226 pub default_input_modes: Vec<String>,
227 pub default_output_modes: Vec<String>,
228 pub skills: Vec<AgentSkill>,
229 #[serde(skip_serializing_if = "Option::is_none")]
230 pub icon_url: Option<String>,
231 #[serde(skip_serializing_if = "Option::is_none")]
232 pub documentation_url: Option<String>,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
237#[serde(rename_all = "camelCase")]
238pub struct AuthenticationInfo {
239 pub scheme: String,
240 pub credentials: String,
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
245#[serde(rename_all = "camelCase")]
246pub struct TaskPushNotificationConfig {
247 pub id: String,
248 pub task_id: String,
249 pub url: String,
250 #[serde(skip_serializing_if = "Option::is_none")]
251 pub token: Option<String>,
252 #[serde(skip_serializing_if = "Option::is_none")]
253 pub authentication: Option<AuthenticationInfo>,
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn part_text_round_trip() {
262 let part = Part::Text {
263 text: "hello".to_string(),
264 metadata: None,
265 };
266 let json = serde_json::to_value(&part).unwrap();
267 assert_eq!(json["kind"], "text");
268 assert_eq!(json["text"], "hello");
269 let deserialized: Part = serde_json::from_value(json).unwrap();
270 assert_eq!(deserialized, part);
271 }
272
273 #[test]
274 fn part_text_with_metadata_round_trip() {
275 let mut meta = HashMap::new();
276 meta.insert("key".to_string(), serde_json::json!("value"));
277 let part = Part::Text {
278 text: "hi".to_string(),
279 metadata: Some(meta),
280 };
281 let json = serde_json::to_value(&part).unwrap();
282 assert_eq!(json["kind"], "text");
283 assert_eq!(json["metadata"]["key"], "value");
284 let deserialized: Part = serde_json::from_value(json).unwrap();
285 assert_eq!(deserialized, part);
286 }
287
288 #[test]
289 fn part_file_round_trip() {
290 let part = Part::File {
291 file: FileContent {
292 name: Some("test.txt".to_string()),
293 mime_type: Some("text/plain".to_string()),
294 bytes: Some("aGVsbG8=".to_string()),
295 url: None,
296 },
297 metadata: None,
298 };
299 let json = serde_json::to_value(&part).unwrap();
300 assert_eq!(json["kind"], "file");
301 assert_eq!(json["file"]["name"], "test.txt");
302 assert_eq!(json["file"]["mimeType"], "text/plain");
303 let deserialized: Part = serde_json::from_value(json).unwrap();
304 assert_eq!(deserialized, part);
305 }
306
307 #[test]
308 fn part_file_url_round_trip() {
309 let part = Part::File {
310 file: FileContent {
311 name: None,
312 mime_type: None,
313 bytes: None,
314 url: Some("https://example.com/file.pdf".to_string()),
315 },
316 metadata: None,
317 };
318 let json = serde_json::to_value(&part).unwrap();
319 assert_eq!(json["kind"], "file");
320 assert_eq!(json["file"]["url"], "https://example.com/file.pdf");
321 assert!(json["file"].get("name").is_none());
323 let deserialized: Part = serde_json::from_value(json).unwrap();
324 assert_eq!(deserialized, part);
325 }
326
327 #[test]
328 fn part_data_round_trip() {
329 let part = Part::Data {
330 data: serde_json::json!({"numbers": [1, 2, 3]}),
331 metadata: None,
332 };
333 let json = serde_json::to_value(&part).unwrap();
334 assert_eq!(json["kind"], "data");
335 assert_eq!(json["data"]["numbers"], serde_json::json!([1, 2, 3]));
336 let deserialized: Part = serde_json::from_value(json).unwrap();
337 assert_eq!(deserialized, part);
338 }
339
340 #[test]
341 fn message_round_trip() {
342 let msg = Message {
343 message_id: "msg-1".to_string(),
344 role: Role::User,
345 parts: vec![Part::Text {
346 text: "hello agent".to_string(),
347 metadata: None,
348 }],
349 context_id: Some("ctx-1".to_string()),
350 task_id: None,
351 metadata: None,
352 reference_task_ids: None,
353 extensions: None,
354 kind: "message".to_string(),
355 };
356 let json = serde_json::to_string(&msg).unwrap();
357 let deserialized: Message = serde_json::from_str(&json).unwrap();
358 assert_eq!(deserialized, msg);
359 }
360
361 #[test]
362 fn message_role_serialization() {
363 let json = serde_json::to_value(Role::User).unwrap();
364 assert_eq!(json, "user");
365 let json = serde_json::to_value(Role::Agent).unwrap();
366 assert_eq!(json, "agent");
367 }
368
369 #[test]
370 fn message_kind_defaults() {
371 let json = serde_json::json!({
373 "messageId": "m1",
374 "role": "agent",
375 "parts": [{"kind": "text", "text": "hi"}]
376 });
377 let msg: Message = serde_json::from_value(json).unwrap();
378 assert_eq!(msg.kind, "message");
379 }
380
381 #[test]
382 fn task_round_trip() {
383 let task = Task {
384 id: "task-1".to_string(),
385 context_id: Some("ctx-1".to_string()),
386 status: TaskStatus {
387 state: TaskState::Working,
388 message: None,
389 timestamp: Some("2025-01-01T00:00:00Z".to_string()),
390 },
391 artifacts: None,
392 history: Some(vec![Message {
393 message_id: "msg-1".to_string(),
394 role: Role::User,
395 parts: vec![Part::Text {
396 text: "do something".to_string(),
397 metadata: None,
398 }],
399 context_id: None,
400 task_id: Some("task-1".to_string()),
401 metadata: None,
402 reference_task_ids: None,
403 extensions: None,
404 kind: "message".to_string(),
405 }]),
406 metadata: None,
407 kind: "task".to_string(),
408 };
409 let json = serde_json::to_string(&task).unwrap();
410 let deserialized: Task =
411 serde_json::from_value(serde_json::from_str(&json).unwrap()).unwrap();
412 assert_eq!(deserialized, task);
413 }
414
415 #[test]
416 fn task_kind_defaults() {
417 let json = serde_json::json!({
418 "id": "t1",
419 "status": {"state": "submitted"}
420 });
421 let task: Task = serde_json::from_value(json).unwrap();
422 assert_eq!(task.kind, "task");
423 }
424
425 #[test]
426 fn task_state_kebab_case_serialization() {
427 let cases = [
428 (TaskState::Unknown, "unknown"),
429 (TaskState::Submitted, "submitted"),
430 (TaskState::Working, "working"),
431 (TaskState::InputRequired, "input-required"),
432 (TaskState::AuthRequired, "auth-required"),
433 (TaskState::Completed, "completed"),
434 (TaskState::Failed, "failed"),
435 (TaskState::Canceled, "canceled"),
436 (TaskState::Rejected, "rejected"),
437 ];
438 for (state, expected) in &cases {
439 let json = serde_json::to_value(state).unwrap();
440 assert_eq!(
441 json.as_str().unwrap(),
442 *expected,
443 "serialization of {state:?}"
444 );
445 let deserialized: TaskState = serde_json::from_value(json).unwrap();
446 assert_eq!(deserialized, *state, "deserialization of {expected}");
447 }
448 }
449
450 #[test]
451 fn task_state_display() {
452 assert_eq!(TaskState::InputRequired.to_string(), "input-required");
453 assert_eq!(TaskState::AuthRequired.to_string(), "auth-required");
454 assert_eq!(TaskState::Completed.to_string(), "completed");
455 }
456
457 #[test]
458 fn agent_card_round_trip() {
459 let card = AgentCard {
460 name: "test-agent".to_string(),
461 description: "A test agent".to_string(),
462 version: "1.0.0".to_string(),
463 url: "https://example.com/a2a/test-agent".to_string(),
464 provider: Some(AgentProvider {
465 organization: "TestOrg".to_string(),
466 url: Some("https://testorg.com".to_string()),
467 }),
468 capabilities: AgentCapabilities {
469 streaming: Some(true),
470 push_notifications: Some(false),
471 },
472 security_schemes: None,
473 security_requirements: None,
474 default_input_modes: vec!["text/plain".to_string()],
475 default_output_modes: vec!["text/plain".to_string()],
476 skills: vec![AgentSkill {
477 id: "summarize".to_string(),
478 name: "Summarize".to_string(),
479 description: "Summarizes text".to_string(),
480 tags: vec!["nlp".to_string()],
481 examples: Some(vec!["Summarize this document".to_string()]),
482 input_modes: None,
483 output_modes: None,
484 }],
485 icon_url: None,
486 documentation_url: None,
487 };
488 let json = serde_json::to_string(&card).unwrap();
489 let deserialized: AgentCard = serde_json::from_str(&json).unwrap();
490 assert_eq!(deserialized, card);
491 }
492
493 #[test]
494 fn agent_card_camel_case_fields() {
495 let card = AgentCard {
496 name: "a".to_string(),
497 description: "d".to_string(),
498 version: "1".to_string(),
499 url: "http://x".to_string(),
500 provider: None,
501 capabilities: AgentCapabilities {
502 streaming: Some(true),
503 push_notifications: Some(true),
504 },
505 security_schemes: None,
506 security_requirements: None,
507 default_input_modes: vec![],
508 default_output_modes: vec![],
509 skills: vec![],
510 icon_url: None,
511 documentation_url: None,
512 };
513 let json = serde_json::to_value(&card).unwrap();
514 assert!(json.get("defaultInputModes").is_some());
516 assert!(json.get("defaultOutputModes").is_some());
517 assert!(json["capabilities"].get("pushNotifications").is_some());
518 }
519
520 #[test]
521 fn task_push_notification_config_round_trip() {
522 let config = TaskPushNotificationConfig {
523 id: "cfg-1".to_string(),
524 task_id: "task-1".to_string(),
525 url: "https://webhook.example.com/notify".to_string(),
526 token: Some("secret-token".to_string()),
527 authentication: Some(AuthenticationInfo {
528 scheme: "Bearer".to_string(),
529 credentials: "my-jwt-token".to_string(),
530 }),
531 };
532 let json = serde_json::to_string(&config).unwrap();
533 let deserialized: TaskPushNotificationConfig = serde_json::from_str(&json).unwrap();
534 assert_eq!(deserialized, config);
535 }
536
537 #[test]
538 fn task_push_notification_config_minimal() {
539 let config = TaskPushNotificationConfig {
540 id: "cfg-2".to_string(),
541 task_id: "task-2".to_string(),
542 url: "https://example.com".to_string(),
543 token: None,
544 authentication: None,
545 };
546 let json = serde_json::to_value(&config).unwrap();
547 assert!(json.get("token").is_none());
549 assert!(json.get("authentication").is_none());
550 let deserialized: TaskPushNotificationConfig = serde_json::from_value(json).unwrap();
551 assert_eq!(deserialized, config);
552 }
553
554 #[test]
555 fn artifact_round_trip() {
556 let artifact = Artifact {
557 artifact_id: "art-1".to_string(),
558 name: Some("output.txt".to_string()),
559 description: Some("Generated output".to_string()),
560 parts: vec![Part::Text {
561 text: "result data".to_string(),
562 metadata: None,
563 }],
564 metadata: None,
565 extensions: None,
566 };
567 let json = serde_json::to_string(&artifact).unwrap();
568 let deserialized: Artifact = serde_json::from_str(&json).unwrap();
569 assert_eq!(deserialized, artifact);
570 }
571
572 #[test]
573 fn security_scheme_api_key() {
574 let scheme = SecurityScheme::ApiKey {
575 name: "x-api-key".to_string(),
576 location: "header".to_string(),
577 };
578 let json = serde_json::to_value(&scheme).unwrap();
579 assert_eq!(json["type"], "apiKey");
580 assert_eq!(json["name"], "x-api-key");
581 assert_eq!(json["in"], "header");
582 let deserialized: SecurityScheme = serde_json::from_value(json).unwrap();
583 assert_eq!(deserialized, scheme);
584 }
585
586 #[test]
587 fn security_scheme_http_bearer() {
588 let scheme = SecurityScheme::Http {
589 scheme: "bearer".to_string(),
590 bearer_format: Some("JWT".to_string()),
591 };
592 let json = serde_json::to_value(&scheme).unwrap();
593 assert_eq!(json["type"], "http");
594 assert_eq!(json["scheme"], "bearer");
595 let bf_key = if json.get("bearerFormat").is_some() {
597 "bearerFormat"
598 } else {
599 "bearer_format"
600 };
601 assert_eq!(json[bf_key], "JWT");
602 let deserialized: SecurityScheme = serde_json::from_value(json).unwrap();
603 assert_eq!(deserialized, scheme);
604 }
605}