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