1use serde::{Deserialize, Serialize};
2use turul_a2a_proto as pb;
3
4use crate::artifact::Artifact;
5use crate::error::A2aTypeError;
6use crate::message::Message;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12#[non_exhaustive]
13pub enum TaskState {
14 Submitted,
15 Working,
16 Completed,
17 Failed,
18 Canceled,
19 InputRequired,
20 Rejected,
21 AuthRequired,
22}
23
24impl TaskState {
25 pub fn can_transition_to(&self, next: TaskState) -> bool {
27 crate::state_machine::validate_transition(*self, next).is_ok()
28 }
29
30 pub fn is_terminal(&self) -> bool {
32 crate::state_machine::is_terminal(*self)
33 }
34}
35
36impl TryFrom<pb::TaskState> for TaskState {
37 type Error = A2aTypeError;
38
39 fn try_from(value: pb::TaskState) -> Result<Self, Self::Error> {
40 match value {
41 pb::TaskState::Submitted => Ok(Self::Submitted),
42 pb::TaskState::Working => Ok(Self::Working),
43 pb::TaskState::Completed => Ok(Self::Completed),
44 pb::TaskState::Failed => Ok(Self::Failed),
45 pb::TaskState::Canceled => Ok(Self::Canceled),
46 pb::TaskState::InputRequired => Ok(Self::InputRequired),
47 pb::TaskState::Rejected => Ok(Self::Rejected),
48 pb::TaskState::AuthRequired => Ok(Self::AuthRequired),
49 pb::TaskState::Unspecified => Err(A2aTypeError::InvalidState),
50 }
51 }
52}
53
54impl From<TaskState> for pb::TaskState {
55 fn from(value: TaskState) -> Self {
56 match value {
57 TaskState::Submitted => pb::TaskState::Submitted,
58 TaskState::Working => pb::TaskState::Working,
59 TaskState::Completed => pb::TaskState::Completed,
60 TaskState::Failed => pb::TaskState::Failed,
61 TaskState::Canceled => pb::TaskState::Canceled,
62 TaskState::InputRequired => pb::TaskState::InputRequired,
63 TaskState::Rejected => pb::TaskState::Rejected,
64 TaskState::AuthRequired => pb::TaskState::AuthRequired,
65 }
66 }
67}
68
69impl TryFrom<i32> for TaskState {
70 type Error = A2aTypeError;
71
72 fn try_from(value: i32) -> Result<Self, Self::Error> {
73 let proto_state = pb::TaskState::try_from(value).map_err(|_| A2aTypeError::InvalidState)?;
74 Self::try_from(proto_state)
75 }
76}
77
78#[derive(Debug, Clone)]
80#[non_exhaustive]
81pub struct TaskStatus {
82 pub(crate) inner: pb::TaskStatus,
83}
84
85impl TaskStatus {
86 pub fn new(state: TaskState) -> Self {
87 Self {
88 inner: pb::TaskStatus {
89 state: pb::TaskState::from(state).into(),
90 message: None,
91 timestamp: None,
92 },
93 }
94 }
95
96 pub fn with_message(mut self, message: Message) -> Self {
97 self.inner.message = Some(message.into_proto());
98 self
99 }
100
101 pub fn state(&self) -> Result<TaskState, A2aTypeError> {
102 let proto_state =
103 pb::TaskState::try_from(self.inner.state).map_err(|_| A2aTypeError::InvalidState)?;
104 TaskState::try_from(proto_state)
105 }
106
107 pub fn as_proto(&self) -> &pb::TaskStatus {
108 &self.inner
109 }
110
111 pub fn into_proto(self) -> pb::TaskStatus {
112 self.inner
113 }
114}
115
116impl TryFrom<pb::TaskStatus> for TaskStatus {
117 type Error = A2aTypeError;
118
119 fn try_from(inner: pb::TaskStatus) -> Result<Self, Self::Error> {
120 let proto_state =
122 pb::TaskState::try_from(inner.state).map_err(|_| A2aTypeError::InvalidState)?;
123 if proto_state == pb::TaskState::Unspecified {
124 return Err(A2aTypeError::InvalidState);
125 }
126 Ok(Self { inner })
127 }
128}
129
130impl From<TaskStatus> for pb::TaskStatus {
131 fn from(status: TaskStatus) -> Self {
132 status.inner
133 }
134}
135
136impl Serialize for TaskStatus {
137 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
138 self.inner.serialize(serializer)
139 }
140}
141
142impl<'de> Deserialize<'de> for TaskStatus {
143 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
144 let proto = pb::TaskStatus::deserialize(deserializer)?;
145 TaskStatus::try_from(proto).map_err(serde::de::Error::custom)
146 }
147}
148
149#[derive(Debug, Clone)]
151#[non_exhaustive]
152pub struct Task {
153 pub(crate) inner: pb::Task,
154}
155
156impl Task {
157 pub fn new(id: impl Into<String>, status: TaskStatus) -> Self {
158 Self {
159 inner: pb::Task {
160 id: id.into(),
161 context_id: String::new(),
162 status: Some(status.into_proto()),
163 artifacts: vec![],
164 history: vec![],
165 metadata: None,
166 },
167 }
168 }
169
170 pub fn with_context_id(mut self, context_id: impl Into<String>) -> Self {
171 self.inner.context_id = context_id.into();
172 self
173 }
174
175 pub fn id(&self) -> &str {
176 &self.inner.id
177 }
178
179 pub fn context_id(&self) -> &str {
180 &self.inner.context_id
181 }
182
183 pub fn status(&self) -> Option<TaskStatus> {
184 self.inner
185 .status
186 .clone()
187 .and_then(|s| TaskStatus::try_from(s).ok())
188 }
189
190 pub fn history(&self) -> &[pb::Message] {
191 &self.inner.history
192 }
193
194 pub fn artifacts(&self) -> &[pb::Artifact] {
195 &self.inner.artifacts
196 }
197
198 pub fn append_message(&mut self, message: Message) {
199 self.inner.history.push(message.into_proto());
200 }
201
202 pub fn append_artifact(&mut self, artifact: Artifact) {
203 self.inner.artifacts.push(artifact.into_proto());
204 }
205
206 pub fn merge_artifact(&mut self, artifact: Artifact, append: bool, _last_chunk: bool) {
225 if append {
226 let target_id = artifact.as_proto().artifact_id.clone();
227 if let Some(existing) = self
228 .inner
229 .artifacts
230 .iter_mut()
231 .find(|a| a.artifact_id == target_id)
232 {
233 existing.parts.extend(artifact.into_proto().parts);
234 return;
235 }
236 }
237 self.append_artifact(artifact);
238 }
239
240 pub fn set_status(&mut self, status: TaskStatus) {
243 self.inner.status = Some(status.into_proto());
244 }
245
246 pub fn complete(&mut self) {
248 self.set_status(TaskStatus::new(TaskState::Completed));
249 }
250
251 pub fn fail(&mut self, message: impl Into<String>) {
253 let msg = Message::new(
254 uuid::Uuid::now_v7().to_string(),
255 crate::Role::Agent,
256 vec![crate::Part::text(message)],
257 );
258 self.set_status(TaskStatus::new(TaskState::Failed).with_message(msg));
259 }
260
261 pub fn push_text_artifact(
263 &mut self,
264 artifact_id: impl Into<String>,
265 name: impl Into<String>,
266 text: impl Into<String>,
267 ) {
268 let artifact = Artifact::new(artifact_id, vec![crate::Part::text(text)]).with_name(name);
269 self.append_artifact(artifact);
270 }
271
272 pub fn as_proto(&self) -> &pb::Task {
273 &self.inner
274 }
275
276 pub fn as_proto_mut(&mut self) -> &mut pb::Task {
277 &mut self.inner
278 }
279
280 pub fn into_proto(self) -> pb::Task {
281 self.inner
282 }
283}
284
285impl TryFrom<pb::Task> for Task {
286 type Error = A2aTypeError;
287
288 fn try_from(inner: pb::Task) -> Result<Self, Self::Error> {
289 if inner.id.is_empty() {
290 return Err(A2aTypeError::MissingField("id"));
291 }
292 let status = inner
294 .status
295 .as_ref()
296 .ok_or(A2aTypeError::MissingField("status"))?;
297 let proto_state =
298 pb::TaskState::try_from(status.state).map_err(|_| A2aTypeError::InvalidState)?;
299 if proto_state == pb::TaskState::Unspecified {
300 return Err(A2aTypeError::InvalidState);
301 }
302 Ok(Self { inner })
303 }
304}
305
306impl From<Task> for pb::Task {
307 fn from(task: Task) -> Self {
308 task.inner
309 }
310}
311
312impl Serialize for Task {
313 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
314 self.inner.serialize(serializer)
315 }
316}
317
318impl<'de> Deserialize<'de> for Task {
319 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
320 let proto = pb::Task::deserialize(deserializer)?;
321 Task::try_from(proto).map_err(serde::de::Error::custom)
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328 use crate::message::{Part, Role};
329
330 #[test]
331 fn try_from_proto_all_valid_states() {
332 assert_eq!(
333 TaskState::try_from(pb::TaskState::Submitted).unwrap(),
334 TaskState::Submitted
335 );
336 assert_eq!(
337 TaskState::try_from(pb::TaskState::Working).unwrap(),
338 TaskState::Working
339 );
340 assert_eq!(
341 TaskState::try_from(pb::TaskState::Completed).unwrap(),
342 TaskState::Completed
343 );
344 assert_eq!(
345 TaskState::try_from(pb::TaskState::Failed).unwrap(),
346 TaskState::Failed
347 );
348 assert_eq!(
349 TaskState::try_from(pb::TaskState::Canceled).unwrap(),
350 TaskState::Canceled
351 );
352 assert_eq!(
353 TaskState::try_from(pb::TaskState::InputRequired).unwrap(),
354 TaskState::InputRequired
355 );
356 assert_eq!(
357 TaskState::try_from(pb::TaskState::Rejected).unwrap(),
358 TaskState::Rejected
359 );
360 assert_eq!(
361 TaskState::try_from(pb::TaskState::AuthRequired).unwrap(),
362 TaskState::AuthRequired
363 );
364 }
365
366 #[test]
367 fn try_from_proto_unspecified_is_error() {
368 assert!(TaskState::try_from(pb::TaskState::Unspecified).is_err());
369 }
370
371 #[test]
372 fn into_proto_round_trip() {
373 for state in [
374 TaskState::Submitted,
375 TaskState::Working,
376 TaskState::Completed,
377 TaskState::Failed,
378 TaskState::Canceled,
379 TaskState::InputRequired,
380 TaskState::Rejected,
381 TaskState::AuthRequired,
382 ] {
383 let proto: pb::TaskState = state.into();
384 let back = TaskState::try_from(proto).unwrap();
385 assert_eq!(back, state);
386 }
387 }
388
389 #[test]
390 fn try_from_i32() {
391 assert_eq!(TaskState::try_from(1i32).unwrap(), TaskState::Submitted);
392 assert_eq!(TaskState::try_from(2i32).unwrap(), TaskState::Working);
393 assert!(TaskState::try_from(0i32).is_err()); assert!(TaskState::try_from(99i32).is_err()); }
396
397 #[test]
398 fn can_transition_to_delegates_to_state_machine() {
399 assert!(TaskState::Submitted.can_transition_to(TaskState::Working));
400 assert!(!TaskState::Submitted.can_transition_to(TaskState::Completed));
401 assert!(!TaskState::Completed.can_transition_to(TaskState::Working));
402 }
403
404 #[test]
405 fn is_terminal_delegates() {
406 assert!(!TaskState::Working.is_terminal());
407 assert!(TaskState::Completed.is_terminal());
408 }
409
410 #[test]
413 fn task_status_constructor() {
414 let status = TaskStatus::new(TaskState::Working);
415 assert_eq!(status.state().unwrap(), TaskState::Working);
416 }
417
418 #[test]
419 fn task_status_with_message() {
420 let msg = crate::Message::new("s-msg", Role::Agent, vec![Part::text("working")]);
421 let status = TaskStatus::new(TaskState::Working).with_message(msg);
422 assert!(status.as_proto().message.is_some());
423 }
424
425 #[test]
426 fn task_status_try_from_proto_rejects_unspecified() {
427 let proto = pb::TaskStatus {
428 state: pb::TaskState::Unspecified.into(),
429 message: None,
430 timestamp: None,
431 };
432 assert!(TaskStatus::try_from(proto).is_err());
433 }
434
435 #[test]
436 fn task_status_serde_round_trip() {
437 let status = TaskStatus::new(TaskState::Submitted);
438 let json = serde_json::to_string(&status).unwrap();
439 let back: TaskStatus = serde_json::from_str(&json).unwrap();
440 assert_eq!(back.state().unwrap(), TaskState::Submitted);
441 }
442
443 #[test]
446 fn task_constructor() {
447 let task = Task::new("t-1", TaskStatus::new(TaskState::Submitted)).with_context_id("ctx-1");
448 assert_eq!(task.id(), "t-1");
449 assert_eq!(task.context_id(), "ctx-1");
450 assert_eq!(
451 task.status().unwrap().state().unwrap(),
452 TaskState::Submitted
453 );
454 }
455
456 #[test]
457 fn task_append_history_and_artifacts() {
458 let mut task = Task::new("t-2", TaskStatus::new(TaskState::Working));
459 task.append_message(crate::Message::new(
460 "m-1",
461 Role::User,
462 vec![Part::text("hi")],
463 ));
464 task.append_artifact(crate::Artifact::new("a-1", vec![Part::text("result")]));
465 assert_eq!(task.history().len(), 1);
466 assert_eq!(task.artifacts().len(), 1);
467 }
468
469 #[test]
470 fn task_merge_artifact_append_true_extends_existing_by_id() {
471 let mut task = Task::new("t-merge-1", TaskStatus::new(TaskState::Working));
472 task.append_artifact(crate::Artifact::new("a-1", vec![Part::text("chunk-1")]));
473
474 task.merge_artifact(
475 crate::Artifact::new("a-1", vec![Part::text("chunk-2")]),
476 true,
477 false,
478 );
479
480 assert_eq!(
481 task.artifacts().len(),
482 1,
483 "same-id append must not duplicate"
484 );
485 assert_eq!(task.artifacts()[0].parts.len(), 2);
486 }
487
488 #[test]
489 fn task_merge_artifact_append_true_no_match_adds_new() {
490 let mut task = Task::new("t-merge-2", TaskStatus::new(TaskState::Working));
491 task.append_artifact(crate::Artifact::new("a-1", vec![Part::text("x")]));
492
493 task.merge_artifact(
494 crate::Artifact::new("a-2", vec![Part::text("y")]),
495 true,
496 false,
497 );
498
499 assert_eq!(task.artifacts().len(), 2);
500 }
501
502 #[test]
503 fn task_merge_artifact_append_false_always_appends() {
504 let mut task = Task::new("t-merge-3", TaskStatus::new(TaskState::Working));
505 task.append_artifact(crate::Artifact::new("a-1", vec![Part::text("x")]));
506
507 task.merge_artifact(
508 crate::Artifact::new("a-1", vec![Part::text("y")]),
509 false,
510 true,
511 );
512
513 assert_eq!(
514 task.artifacts().len(),
515 2,
516 "append=false does not merge by id"
517 );
518 }
519
520 #[test]
521 fn task_try_from_proto_rejects_empty_id() {
522 let proto = pb::Task {
523 id: String::new(),
524 context_id: String::new(),
525 status: Some(pb::TaskStatus {
526 state: pb::TaskState::Submitted.into(),
527 message: None,
528 timestamp: None,
529 }),
530 artifacts: vec![],
531 history: vec![],
532 metadata: None,
533 };
534 assert!(Task::try_from(proto).is_err());
535 }
536
537 #[test]
538 fn task_try_from_proto_rejects_missing_status() {
539 let proto = pb::Task {
541 id: "t-no-status".to_string(),
542 context_id: String::new(),
543 status: None,
544 artifacts: vec![],
545 history: vec![],
546 metadata: None,
547 };
548 assert!(Task::try_from(proto).is_err());
549 }
550
551 #[test]
552 fn task_try_from_proto_rejects_unspecified_state() {
553 let proto = pb::Task {
554 id: "t-bad".to_string(),
555 context_id: String::new(),
556 status: Some(pb::TaskStatus {
557 state: pb::TaskState::Unspecified.into(),
558 message: None,
559 timestamp: None,
560 }),
561 artifacts: vec![],
562 history: vec![],
563 metadata: None,
564 };
565 assert!(Task::try_from(proto).is_err());
566 }
567
568 #[test]
569 fn task_serde_round_trip() {
570 let task = Task::new("t-rt", TaskStatus::new(TaskState::Working)).with_context_id("ctx-rt");
571 let json = serde_json::to_string(&task).unwrap();
572 let back: Task = serde_json::from_str(&json).unwrap();
573 assert_eq!(back.id(), "t-rt");
574 assert_eq!(back.context_id(), "ctx-rt");
575 }
576
577 #[test]
580 fn task_complete_sets_completed_status() {
581 let mut task = Task::new("h-1", TaskStatus::new(TaskState::Submitted));
582 task.complete();
583 assert_eq!(
584 task.status().unwrap().state().unwrap(),
585 TaskState::Completed
586 );
587 }
588
589 #[test]
590 fn task_fail_sets_failed_status_with_message() {
591 let mut task = Task::new("h-2", TaskStatus::new(TaskState::Submitted));
592 task.fail("something went wrong");
593 let status = task.status().unwrap();
594 assert_eq!(status.state().unwrap(), TaskState::Failed);
595 assert!(status.as_proto().message.is_some());
597 }
598
599 #[test]
600 fn task_set_status_generic() {
601 let mut task = Task::new("h-3", TaskStatus::new(TaskState::Submitted));
602 task.set_status(TaskStatus::new(TaskState::Working));
603 assert_eq!(task.status().unwrap().state().unwrap(), TaskState::Working);
604 }
605
606 #[test]
607 fn task_push_text_artifact() {
608 let mut task = Task::new("h-4", TaskStatus::new(TaskState::Submitted));
609 task.push_text_artifact("art-1", "Result", "hello world");
610 assert_eq!(task.artifacts().len(), 1);
611 assert_eq!(task.artifacts()[0].artifact_id, "art-1");
612 assert_eq!(task.artifacts()[0].name, "Result");
613 assert_eq!(task.artifacts()[0].parts.len(), 1);
614 }
615}