1use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7pub enum TaskState {
8 #[serde(rename = "submitted")]
9 Submitted,
10 #[serde(rename = "working")]
11 Working,
12 #[serde(rename = "input-required")]
13 InputRequired,
14 #[serde(rename = "completed")]
15 Completed,
16 #[serde(rename = "failed")]
17 Failed,
18 #[serde(rename = "canceled")]
19 Canceled,
20 #[serde(rename = "rejected")]
21 Rejected,
22 #[serde(rename = "auth-required")]
23 AuthRequired,
24 #[serde(rename = "unknown")]
25 Unknown,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29#[serde(rename_all = "camelCase")]
30pub struct Task {
31 pub id: String,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub context_id: Option<String>,
34 pub status: TaskStatus,
35 #[serde(default, skip_serializing_if = "Vec::is_empty")]
36 pub artifacts: Vec<Artifact>,
37 #[serde(default, skip_serializing_if = "Vec::is_empty")]
38 pub history: Vec<Message>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub metadata: Option<serde_json::Value>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub struct TaskStatus {
46 pub state: TaskState,
47 pub timestamp: String,
48 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub message: Option<Message>,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(rename_all = "lowercase")]
54pub enum Role {
55 User,
56 Agent,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60#[serde(rename_all = "camelCase")]
61pub struct Message {
62 pub role: Role,
63 pub parts: Vec<Part>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub message_id: Option<String>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub task_id: Option<String>,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub context_id: Option<String>,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub metadata: Option<serde_json::Value>,
72}
73
74#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
75#[serde(tag = "kind", rename_all = "lowercase")]
76pub enum Part {
77 Text {
78 text: String,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
80 metadata: Option<serde_json::Value>,
81 },
82 File {
83 file: FileContent,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
85 metadata: Option<serde_json::Value>,
86 },
87 Data {
88 data: serde_json::Value,
89 #[serde(default, skip_serializing_if = "Option::is_none")]
90 metadata: Option<serde_json::Value>,
91 },
92}
93
94#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
95#[serde(rename_all = "camelCase")]
96pub struct FileContent {
97 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub name: Option<String>,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub media_type: Option<String>,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub file_with_bytes: Option<String>,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub file_with_uri: Option<String>,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
108#[serde(rename_all = "camelCase")]
109pub struct Artifact {
110 pub artifact_id: String,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub name: Option<String>,
113 pub parts: Vec<Part>,
114 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub metadata: Option<serde_json::Value>,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
119#[serde(rename_all = "camelCase")]
120pub struct AgentCard {
121 pub name: String,
122 pub description: String,
123 pub url: String,
124 pub version: String,
125 pub protocol_version: String,
126 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub provider: Option<AgentProvider>,
128 pub capabilities: AgentCapabilities,
129 #[serde(default, skip_serializing_if = "Vec::is_empty")]
130 pub default_input_modes: Vec<String>,
131 #[serde(default, skip_serializing_if = "Vec::is_empty")]
132 pub default_output_modes: Vec<String>,
133 #[serde(default, skip_serializing_if = "Vec::is_empty")]
134 pub skills: Vec<AgentSkill>,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
138#[serde(rename_all = "camelCase")]
139pub struct AgentProvider {
140 pub organization: String,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub url: Option<String>,
143}
144
145#[derive(Debug, Clone, Default, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase")]
147pub struct AgentCapabilities {
148 #[serde(default)]
149 pub streaming: bool,
150 #[serde(default)]
151 pub push_notifications: bool,
152 #[serde(default)]
153 pub state_transition_history: bool,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
157#[serde(rename_all = "camelCase")]
158pub struct AgentSkill {
159 pub id: String,
160 pub name: String,
161 pub description: String,
162 #[serde(default, skip_serializing_if = "Vec::is_empty")]
163 pub tags: Vec<String>,
164 #[serde(default, skip_serializing_if = "Vec::is_empty")]
165 pub examples: Vec<String>,
166 #[serde(default, skip_serializing_if = "Vec::is_empty")]
167 pub input_modes: Vec<String>,
168 #[serde(default, skip_serializing_if = "Vec::is_empty")]
169 pub output_modes: Vec<String>,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
173#[serde(rename_all = "camelCase")]
174pub struct TaskStatusUpdateEvent {
175 #[serde(default = "kind_status_update")]
176 pub kind: String,
177 pub task_id: String,
178 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub context_id: Option<String>,
180 pub status: TaskStatus,
181 #[serde(rename = "final", default)]
182 pub is_final: bool,
183}
184
185fn kind_status_update() -> String {
186 "status-update".into()
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
190#[serde(rename_all = "camelCase")]
191pub struct TaskArtifactUpdateEvent {
192 #[serde(default = "kind_artifact_update")]
193 pub kind: String,
194 pub task_id: String,
195 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub context_id: Option<String>,
197 pub artifact: Artifact,
198 #[serde(rename = "final", default)]
199 pub is_final: bool,
200}
201
202fn kind_artifact_update() -> String {
203 "artifact-update".into()
204}
205
206impl Part {
207 #[must_use]
208 pub fn text(s: impl Into<String>) -> Self {
209 Self::Text {
210 text: s.into(),
211 metadata: None,
212 }
213 }
214}
215
216impl Message {
217 #[must_use]
218 pub fn user_text(s: impl Into<String>) -> Self {
219 Self {
220 role: Role::User,
221 parts: vec![Part::text(s)],
222 message_id: None,
223 task_id: None,
224 context_id: None,
225 metadata: None,
226 }
227 }
228
229 #[must_use]
230 pub fn text_content(&self) -> Option<&str> {
231 self.parts.iter().find_map(|p| match p {
232 Part::Text { text, .. } => Some(text.as_str()),
233 _ => None,
234 })
235 }
236
237 #[must_use]
243 pub fn all_text_content(&self) -> String {
244 let parts: Vec<&str> = self
245 .parts
246 .iter()
247 .filter_map(|p| match p {
248 Part::Text { text, .. } => Some(text.as_str()),
249 _ => None,
250 })
251 .collect();
252 parts.join("\n\n")
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn task_state_serde() {
262 let states = [
263 (TaskState::Submitted, "\"submitted\""),
264 (TaskState::Working, "\"working\""),
265 (TaskState::InputRequired, "\"input-required\""),
266 (TaskState::Completed, "\"completed\""),
267 (TaskState::Failed, "\"failed\""),
268 (TaskState::Canceled, "\"canceled\""),
269 (TaskState::Rejected, "\"rejected\""),
270 (TaskState::AuthRequired, "\"auth-required\""),
271 (TaskState::Unknown, "\"unknown\""),
272 ];
273 for (state, expected) in states {
274 let json = serde_json::to_string(&state).unwrap();
275 assert_eq!(json, expected, "serialization mismatch for {state:?}");
276 let back: TaskState = serde_json::from_str(&json).unwrap();
277 assert_eq!(back, state);
278 }
279 }
280
281 #[test]
282 fn role_serde_lowercase() {
283 assert_eq!(serde_json::to_string(&Role::User).unwrap(), "\"user\"");
284 assert_eq!(serde_json::to_string(&Role::Agent).unwrap(), "\"agent\"");
285 }
286
287 #[test]
288 fn part_text_constructor() {
289 let part = Part::text("hello");
290 assert_eq!(
291 part,
292 Part::Text {
293 text: "hello".into(),
294 metadata: None
295 }
296 );
297 }
298
299 #[test]
300 fn part_kind_serde() {
301 let text_part = Part::text("hello");
302 let json = serde_json::to_string(&text_part).unwrap();
303 assert!(json.contains("\"kind\":\"text\""));
304 assert!(json.contains("\"text\":\"hello\""));
305 let back: Part = serde_json::from_str(&json).unwrap();
306 assert_eq!(back, text_part);
307
308 let file_part = Part::File {
309 file: FileContent {
310 name: Some("doc.pdf".into()),
311 media_type: None,
312 file_with_bytes: None,
313 file_with_uri: Some("https://example.com/doc.pdf".into()),
314 },
315 metadata: None,
316 };
317 let json = serde_json::to_string(&file_part).unwrap();
318 assert!(json.contains("\"kind\":\"file\""));
319 let back: Part = serde_json::from_str(&json).unwrap();
320 assert_eq!(back, file_part);
321
322 let data_part = Part::Data {
323 data: serde_json::json!({"key": "value"}),
324 metadata: None,
325 };
326 let json = serde_json::to_string(&data_part).unwrap();
327 assert!(json.contains("\"kind\":\"data\""));
328 let back: Part = serde_json::from_str(&json).unwrap();
329 assert_eq!(back, data_part);
330 }
331
332 #[test]
333 fn message_user_text_constructor() {
334 let msg = Message::user_text("test input");
335 assert_eq!(msg.role, Role::User);
336 assert_eq!(msg.text_content(), Some("test input"));
337 }
338
339 #[test]
340 fn message_serde_round_trip() {
341 let msg = Message::user_text("hello agent");
342 let json = serde_json::to_string(&msg).unwrap();
343 let back: Message = serde_json::from_str(&json).unwrap();
344 assert_eq!(back.role, Role::User);
345 assert_eq!(back.text_content(), Some("hello agent"));
346 }
347
348 #[test]
349 fn task_serde_round_trip() {
350 let task = Task {
351 id: "task-1".into(),
352 context_id: None,
353 status: TaskStatus {
354 state: TaskState::Working,
355 timestamp: "2025-01-01T00:00:00Z".into(),
356 message: None,
357 },
358 artifacts: vec![],
359 history: vec![Message::user_text("do something")],
360 metadata: None,
361 };
362 let json = serde_json::to_string(&task).unwrap();
363 assert!(json.contains("\"contextId\"").not());
364 let back: Task = serde_json::from_str(&json).unwrap();
365 assert_eq!(back.id, "task-1");
366 assert_eq!(back.status.state, TaskState::Working);
367 assert_eq!(back.history.len(), 1);
368 }
369
370 #[test]
371 fn task_skips_empty_vecs_and_none() {
372 let task = Task {
373 id: "t".into(),
374 context_id: None,
375 status: TaskStatus {
376 state: TaskState::Submitted,
377 timestamp: "ts".into(),
378 message: None,
379 },
380 artifacts: vec![],
381 history: vec![],
382 metadata: None,
383 };
384 let json = serde_json::to_string(&task).unwrap();
385 assert!(!json.contains("artifacts"));
386 assert!(!json.contains("history"));
387 assert!(!json.contains("metadata"));
388 assert!(!json.contains("contextId"));
389 }
390
391 #[test]
392 fn artifact_serde_round_trip() {
393 let artifact = Artifact {
394 artifact_id: "art-1".into(),
395 name: Some("result.txt".into()),
396 parts: vec![Part::text("file content")],
397 metadata: None,
398 };
399 let json = serde_json::to_string(&artifact).unwrap();
400 assert!(json.contains("\"artifactId\""));
401 let back: Artifact = serde_json::from_str(&json).unwrap();
402 assert_eq!(back.artifact_id, "art-1");
403 }
404
405 #[test]
406 fn agent_card_serde_round_trip() {
407 let card = AgentCard {
408 name: "test-agent".into(),
409 description: "A test agent".into(),
410 url: "http://localhost:8080".into(),
411 version: "0.1.0".into(),
412 protocol_version: "0.2.1".into(),
413 provider: Some(AgentProvider {
414 organization: "TestOrg".into(),
415 url: Some("https://test.org".into()),
416 }),
417 capabilities: AgentCapabilities {
418 streaming: true,
419 push_notifications: false,
420 state_transition_history: false,
421 },
422 default_input_modes: vec!["text".into()],
423 default_output_modes: vec!["text".into()],
424 skills: vec![AgentSkill {
425 id: "skill-1".into(),
426 name: "Test Skill".into(),
427 description: "Does testing".into(),
428 tags: vec!["test".into()],
429 examples: vec![],
430 input_modes: vec![],
431 output_modes: vec![],
432 }],
433 };
434 let json = serde_json::to_string_pretty(&card).unwrap();
435 let back: AgentCard = serde_json::from_str(&json).unwrap();
436 assert_eq!(back.name, "test-agent");
437 assert!(back.capabilities.streaming);
438 assert_eq!(back.skills.len(), 1);
439 }
440
441 #[test]
442 fn task_status_update_event_serde() {
443 let event = TaskStatusUpdateEvent {
444 kind: "status-update".into(),
445 task_id: "t-1".into(),
446 context_id: None,
447 status: TaskStatus {
448 state: TaskState::Completed,
449 timestamp: "ts".into(),
450 message: None,
451 },
452 is_final: true,
453 };
454 let json = serde_json::to_string(&event).unwrap();
455 assert!(json.contains("\"final\":true"));
456 assert!(!json.contains("isFinal"));
457 assert!(json.contains("\"kind\":\"status-update\""));
458 let back: TaskStatusUpdateEvent = serde_json::from_str(&json).unwrap();
459 assert!(back.is_final);
460 assert_eq!(back.kind, "status-update");
461 }
462
463 #[test]
464 fn task_artifact_update_event_serde() {
465 let event = TaskArtifactUpdateEvent {
466 kind: "artifact-update".into(),
467 task_id: "t-1".into(),
468 context_id: None,
469 artifact: Artifact {
470 artifact_id: "a-1".into(),
471 name: None,
472 parts: vec![Part::text("data")],
473 metadata: None,
474 },
475 is_final: false,
476 };
477 let json = serde_json::to_string(&event).unwrap();
478 assert!(json.contains("\"final\":false"));
479 assert!(json.contains("\"kind\":\"artifact-update\""));
480 let back: TaskArtifactUpdateEvent = serde_json::from_str(&json).unwrap();
481 assert!(!back.is_final);
482 assert_eq!(back.kind, "artifact-update");
483 }
484
485 #[test]
486 fn file_content_serde() {
487 let fc = FileContent {
488 name: Some("doc.pdf".into()),
489 media_type: Some("application/pdf".into()),
490 file_with_bytes: Some("base64data==".into()),
491 file_with_uri: None,
492 };
493 let json = serde_json::to_string(&fc).unwrap();
494 assert!(json.contains("\"mediaType\""));
495 assert!(json.contains("\"fileWithBytes\""));
496 assert!(!json.contains("fileWithUri"));
497 let back: FileContent = serde_json::from_str(&json).unwrap();
498 assert_eq!(back.name.as_deref(), Some("doc.pdf"));
499 }
500
501 #[test]
502 fn all_text_content_single_part() {
503 let msg = Message::user_text("hello world");
504 assert_eq!(msg.all_text_content(), "hello world");
505 }
506
507 #[test]
508 fn all_text_content_multiple_parts_joined() {
509 let msg = Message {
510 role: Role::User,
511 parts: vec![
512 Part::text("first"),
513 Part::text("second"),
514 Part::text("third"),
515 ],
516 message_id: None,
517 task_id: None,
518 context_id: None,
519 metadata: None,
520 };
521 assert_eq!(msg.all_text_content(), "first\n\nsecond\n\nthird");
522 }
523
524 #[test]
525 fn all_text_content_no_text_parts_returns_empty() {
526 let msg = Message {
527 role: Role::User,
528 parts: vec![],
529 message_id: None,
530 task_id: None,
531 context_id: None,
532 metadata: None,
533 };
534 assert_eq!(msg.all_text_content(), "");
535 }
536
537 #[test]
538 fn all_text_content_skips_non_text_parts() {
539 let msg = Message {
540 role: Role::User,
541 parts: vec![
542 Part::text("text-only"),
543 Part::Data {
544 data: serde_json::json!({"key": "val"}),
545 metadata: None,
546 },
547 ],
548 message_id: None,
549 task_id: None,
550 context_id: None,
551 metadata: None,
552 };
553 assert_eq!(msg.all_text_content(), "text-only");
554 }
555
556 trait Not {
557 fn not(&self) -> bool;
558 }
559 impl Not for bool {
560 fn not(&self) -> bool {
561 !*self
562 }
563 }
564}