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 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub provider: Option<AgentProvider>,
127 pub capabilities: AgentCapabilities,
128 #[serde(default, skip_serializing_if = "Vec::is_empty")]
129 pub default_input_modes: Vec<String>,
130 #[serde(default, skip_serializing_if = "Vec::is_empty")]
131 pub default_output_modes: Vec<String>,
132 #[serde(default, skip_serializing_if = "Vec::is_empty")]
133 pub skills: Vec<AgentSkill>,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
137#[serde(rename_all = "camelCase")]
138pub struct AgentProvider {
139 pub organization: String,
140 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub url: Option<String>,
142}
143
144#[derive(Debug, Clone, Default, Serialize, Deserialize)]
145#[serde(rename_all = "camelCase")]
146pub struct AgentCapabilities {
147 #[serde(default)]
148 pub streaming: bool,
149 #[serde(default)]
150 pub push_notifications: bool,
151 #[serde(default)]
152 pub state_transition_history: bool,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
156#[serde(rename_all = "camelCase")]
157pub struct AgentSkill {
158 pub id: String,
159 pub name: String,
160 pub description: String,
161 #[serde(default, skip_serializing_if = "Vec::is_empty")]
162 pub tags: Vec<String>,
163 #[serde(default, skip_serializing_if = "Vec::is_empty")]
164 pub examples: Vec<String>,
165 #[serde(default, skip_serializing_if = "Vec::is_empty")]
166 pub input_modes: Vec<String>,
167 #[serde(default, skip_serializing_if = "Vec::is_empty")]
168 pub output_modes: Vec<String>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct TaskStatusUpdateEvent {
174 #[serde(default = "kind_status_update")]
175 pub kind: String,
176 pub task_id: String,
177 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub context_id: Option<String>,
179 pub status: TaskStatus,
180 #[serde(rename = "final", default)]
181 pub is_final: bool,
182}
183
184fn kind_status_update() -> String {
185 "status-update".into()
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
189#[serde(rename_all = "camelCase")]
190pub struct TaskArtifactUpdateEvent {
191 #[serde(default = "kind_artifact_update")]
192 pub kind: String,
193 pub task_id: String,
194 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub context_id: Option<String>,
196 pub artifact: Artifact,
197 #[serde(rename = "final", default)]
198 pub is_final: bool,
199}
200
201fn kind_artifact_update() -> String {
202 "artifact-update".into()
203}
204
205impl Part {
206 #[must_use]
207 pub fn text(s: impl Into<String>) -> Self {
208 Self::Text {
209 text: s.into(),
210 metadata: None,
211 }
212 }
213}
214
215impl Message {
216 #[must_use]
217 pub fn user_text(s: impl Into<String>) -> Self {
218 Self {
219 role: Role::User,
220 parts: vec![Part::text(s)],
221 message_id: None,
222 task_id: None,
223 context_id: None,
224 metadata: None,
225 }
226 }
227
228 #[must_use]
229 pub fn text_content(&self) -> Option<&str> {
230 self.parts.iter().find_map(|p| match p {
231 Part::Text { text, .. } => Some(text.as_str()),
232 _ => None,
233 })
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn task_state_serde() {
243 let states = [
244 (TaskState::Submitted, "\"submitted\""),
245 (TaskState::Working, "\"working\""),
246 (TaskState::InputRequired, "\"input-required\""),
247 (TaskState::Completed, "\"completed\""),
248 (TaskState::Failed, "\"failed\""),
249 (TaskState::Canceled, "\"canceled\""),
250 (TaskState::Rejected, "\"rejected\""),
251 (TaskState::AuthRequired, "\"auth-required\""),
252 (TaskState::Unknown, "\"unknown\""),
253 ];
254 for (state, expected) in states {
255 let json = serde_json::to_string(&state).unwrap();
256 assert_eq!(json, expected, "serialization mismatch for {state:?}");
257 let back: TaskState = serde_json::from_str(&json).unwrap();
258 assert_eq!(back, state);
259 }
260 }
261
262 #[test]
263 fn role_serde_lowercase() {
264 assert_eq!(serde_json::to_string(&Role::User).unwrap(), "\"user\"");
265 assert_eq!(serde_json::to_string(&Role::Agent).unwrap(), "\"agent\"");
266 }
267
268 #[test]
269 fn part_text_constructor() {
270 let part = Part::text("hello");
271 assert_eq!(
272 part,
273 Part::Text {
274 text: "hello".into(),
275 metadata: None
276 }
277 );
278 }
279
280 #[test]
281 fn part_kind_serde() {
282 let text_part = Part::text("hello");
283 let json = serde_json::to_string(&text_part).unwrap();
284 assert!(json.contains("\"kind\":\"text\""));
285 assert!(json.contains("\"text\":\"hello\""));
286 let back: Part = serde_json::from_str(&json).unwrap();
287 assert_eq!(back, text_part);
288
289 let file_part = Part::File {
290 file: FileContent {
291 name: Some("doc.pdf".into()),
292 media_type: None,
293 file_with_bytes: None,
294 file_with_uri: Some("https://example.com/doc.pdf".into()),
295 },
296 metadata: None,
297 };
298 let json = serde_json::to_string(&file_part).unwrap();
299 assert!(json.contains("\"kind\":\"file\""));
300 let back: Part = serde_json::from_str(&json).unwrap();
301 assert_eq!(back, file_part);
302
303 let data_part = Part::Data {
304 data: serde_json::json!({"key": "value"}),
305 metadata: None,
306 };
307 let json = serde_json::to_string(&data_part).unwrap();
308 assert!(json.contains("\"kind\":\"data\""));
309 let back: Part = serde_json::from_str(&json).unwrap();
310 assert_eq!(back, data_part);
311 }
312
313 #[test]
314 fn message_user_text_constructor() {
315 let msg = Message::user_text("test input");
316 assert_eq!(msg.role, Role::User);
317 assert_eq!(msg.text_content(), Some("test input"));
318 }
319
320 #[test]
321 fn message_serde_round_trip() {
322 let msg = Message::user_text("hello agent");
323 let json = serde_json::to_string(&msg).unwrap();
324 let back: Message = serde_json::from_str(&json).unwrap();
325 assert_eq!(back.role, Role::User);
326 assert_eq!(back.text_content(), Some("hello agent"));
327 }
328
329 #[test]
330 fn task_serde_round_trip() {
331 let task = Task {
332 id: "task-1".into(),
333 context_id: None,
334 status: TaskStatus {
335 state: TaskState::Working,
336 timestamp: "2025-01-01T00:00:00Z".into(),
337 message: None,
338 },
339 artifacts: vec![],
340 history: vec![Message::user_text("do something")],
341 metadata: None,
342 };
343 let json = serde_json::to_string(&task).unwrap();
344 assert!(json.contains("\"contextId\"").not());
345 let back: Task = serde_json::from_str(&json).unwrap();
346 assert_eq!(back.id, "task-1");
347 assert_eq!(back.status.state, TaskState::Working);
348 assert_eq!(back.history.len(), 1);
349 }
350
351 #[test]
352 fn task_skips_empty_vecs_and_none() {
353 let task = Task {
354 id: "t".into(),
355 context_id: None,
356 status: TaskStatus {
357 state: TaskState::Submitted,
358 timestamp: "ts".into(),
359 message: None,
360 },
361 artifacts: vec![],
362 history: vec![],
363 metadata: None,
364 };
365 let json = serde_json::to_string(&task).unwrap();
366 assert!(!json.contains("artifacts"));
367 assert!(!json.contains("history"));
368 assert!(!json.contains("metadata"));
369 assert!(!json.contains("contextId"));
370 }
371
372 #[test]
373 fn artifact_serde_round_trip() {
374 let artifact = Artifact {
375 artifact_id: "art-1".into(),
376 name: Some("result.txt".into()),
377 parts: vec![Part::text("file content")],
378 metadata: None,
379 };
380 let json = serde_json::to_string(&artifact).unwrap();
381 assert!(json.contains("\"artifactId\""));
382 let back: Artifact = serde_json::from_str(&json).unwrap();
383 assert_eq!(back.artifact_id, "art-1");
384 }
385
386 #[test]
387 fn agent_card_serde_round_trip() {
388 let card = AgentCard {
389 name: "test-agent".into(),
390 description: "A test agent".into(),
391 url: "http://localhost:8080".into(),
392 version: "0.1.0".into(),
393 provider: Some(AgentProvider {
394 organization: "TestOrg".into(),
395 url: Some("https://test.org".into()),
396 }),
397 capabilities: AgentCapabilities {
398 streaming: true,
399 push_notifications: false,
400 state_transition_history: false,
401 },
402 default_input_modes: vec!["text".into()],
403 default_output_modes: vec!["text".into()],
404 skills: vec![AgentSkill {
405 id: "skill-1".into(),
406 name: "Test Skill".into(),
407 description: "Does testing".into(),
408 tags: vec!["test".into()],
409 examples: vec![],
410 input_modes: vec![],
411 output_modes: vec![],
412 }],
413 };
414 let json = serde_json::to_string_pretty(&card).unwrap();
415 let back: AgentCard = serde_json::from_str(&json).unwrap();
416 assert_eq!(back.name, "test-agent");
417 assert!(back.capabilities.streaming);
418 assert_eq!(back.skills.len(), 1);
419 }
420
421 #[test]
422 fn task_status_update_event_serde() {
423 let event = TaskStatusUpdateEvent {
424 kind: "status-update".into(),
425 task_id: "t-1".into(),
426 context_id: None,
427 status: TaskStatus {
428 state: TaskState::Completed,
429 timestamp: "ts".into(),
430 message: None,
431 },
432 is_final: true,
433 };
434 let json = serde_json::to_string(&event).unwrap();
435 assert!(json.contains("\"final\":true"));
436 assert!(!json.contains("isFinal"));
437 assert!(json.contains("\"kind\":\"status-update\""));
438 let back: TaskStatusUpdateEvent = serde_json::from_str(&json).unwrap();
439 assert!(back.is_final);
440 assert_eq!(back.kind, "status-update");
441 }
442
443 #[test]
444 fn task_artifact_update_event_serde() {
445 let event = TaskArtifactUpdateEvent {
446 kind: "artifact-update".into(),
447 task_id: "t-1".into(),
448 context_id: None,
449 artifact: Artifact {
450 artifact_id: "a-1".into(),
451 name: None,
452 parts: vec![Part::text("data")],
453 metadata: None,
454 },
455 is_final: false,
456 };
457 let json = serde_json::to_string(&event).unwrap();
458 assert!(json.contains("\"final\":false"));
459 assert!(json.contains("\"kind\":\"artifact-update\""));
460 let back: TaskArtifactUpdateEvent = serde_json::from_str(&json).unwrap();
461 assert!(!back.is_final);
462 assert_eq!(back.kind, "artifact-update");
463 }
464
465 #[test]
466 fn file_content_serde() {
467 let fc = FileContent {
468 name: Some("doc.pdf".into()),
469 media_type: Some("application/pdf".into()),
470 file_with_bytes: Some("base64data==".into()),
471 file_with_uri: None,
472 };
473 let json = serde_json::to_string(&fc).unwrap();
474 assert!(json.contains("\"mediaType\""));
475 assert!(json.contains("\"fileWithBytes\""));
476 assert!(!json.contains("fileWithUri"));
477 let back: FileContent = serde_json::from_str(&json).unwrap();
478 assert_eq!(back.name.as_deref(), Some("doc.pdf"));
479 }
480
481 trait Not {
482 fn not(&self) -> bool;
483 }
484 impl Not for bool {
485 fn not(&self) -> bool {
486 !*self
487 }
488 }
489}