Skip to main content

rullama_a2a/
task.rs

1//! Task lifecycle types: Task, TaskStatus, TaskState.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6use crate::types::{Artifact, Message};
7
8/// Possible lifecycle states of a Task.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum TaskState {
11    /// Unspecified or indeterminate state.
12    #[serde(rename = "TASK_STATE_UNSPECIFIED")]
13    Unspecified,
14    /// Task has been submitted and acknowledged.
15    #[serde(rename = "TASK_STATE_SUBMITTED")]
16    Submitted,
17    /// Task is actively being processed.
18    #[serde(rename = "TASK_STATE_WORKING")]
19    Working,
20    /// Task finished successfully (terminal).
21    #[serde(rename = "TASK_STATE_COMPLETED")]
22    Completed,
23    /// Task finished with an error (terminal).
24    #[serde(rename = "TASK_STATE_FAILED")]
25    Failed,
26    /// Task was canceled (terminal).
27    #[serde(rename = "TASK_STATE_CANCELED")]
28    Canceled,
29    /// Task was rejected by the agent (terminal).
30    #[serde(rename = "TASK_STATE_REJECTED")]
31    Rejected,
32    /// Agent requires additional user input (interrupted).
33    #[serde(rename = "TASK_STATE_INPUT_REQUIRED")]
34    InputRequired,
35    /// Authentication is required to proceed (interrupted).
36    #[serde(rename = "TASK_STATE_AUTH_REQUIRED")]
37    AuthRequired,
38}
39
40/// Current status of a task.
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
42pub struct TaskStatus {
43    /// Current state.
44    pub state: TaskState,
45    /// Optional message associated with the status.
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub message: Option<Message>,
48    /// ISO 8601 timestamp when the status was recorded.
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub timestamp: Option<String>,
51}
52
53/// The core unit of action in A2A.
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
55pub struct Task {
56    /// Unique task identifier (UUID).
57    pub id: String,
58    /// Context identifier for the conversation/session.
59    #[serde(rename = "contextId", skip_serializing_if = "Option::is_none")]
60    pub context_id: Option<String>,
61    /// Current task status.
62    pub status: TaskStatus,
63    /// Output artifacts.
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub artifacts: Option<Vec<Artifact>>,
66    /// History of interactions.
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub history: Option<Vec<Message>>,
69    /// Custom metadata.
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub metadata: Option<HashMap<String, serde_json::Value>>,
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    fn submitted_task(id: &str) -> Task {
79        Task {
80            id: id.to_string(),
81            context_id: None,
82            status: TaskStatus {
83                state: TaskState::Submitted,
84                message: None,
85                timestamp: None,
86            },
87            artifacts: None,
88            history: None,
89            metadata: None,
90        }
91    }
92
93    // --- TaskState ---
94
95    #[test]
96    fn task_state_serializes_to_uppercase_snake() {
97        assert_eq!(
98            serde_json::to_string(&TaskState::Submitted).unwrap(),
99            r#""TASK_STATE_SUBMITTED""#
100        );
101        assert_eq!(
102            serde_json::to_string(&TaskState::Completed).unwrap(),
103            r#""TASK_STATE_COMPLETED""#
104        );
105        assert_eq!(
106            serde_json::to_string(&TaskState::Failed).unwrap(),
107            r#""TASK_STATE_FAILED""#
108        );
109    }
110
111    #[test]
112    fn all_task_states_roundtrip() {
113        let states = [
114            TaskState::Unspecified,
115            TaskState::Submitted,
116            TaskState::Working,
117            TaskState::Completed,
118            TaskState::Failed,
119            TaskState::Canceled,
120            TaskState::Rejected,
121            TaskState::InputRequired,
122            TaskState::AuthRequired,
123        ];
124        for state in states {
125            let json = serde_json::to_string(&state).unwrap();
126            let back: TaskState = serde_json::from_str(&json).unwrap();
127            assert_eq!(back, state);
128        }
129    }
130
131    // --- TaskStatus ---
132
133    #[test]
134    fn task_status_roundtrip() {
135        let status = TaskStatus {
136            state: TaskState::Working,
137            message: None,
138            timestamp: Some("2024-01-01T00:00:00Z".to_string()),
139        };
140        let json = serde_json::to_string(&status).unwrap();
141        let back: TaskStatus = serde_json::from_str(&json).unwrap();
142        assert_eq!(back, status);
143    }
144
145    #[test]
146    fn task_status_omits_optional_fields_when_none() {
147        let status = TaskStatus {
148            state: TaskState::Submitted,
149            message: None,
150            timestamp: None,
151        };
152        let json = serde_json::to_string(&status).unwrap();
153        assert!(!json.contains("message"));
154        assert!(!json.contains("timestamp"));
155    }
156
157    // --- Task ---
158
159    #[test]
160    fn task_roundtrip_minimal() {
161        let task = submitted_task("task-001");
162        let json = serde_json::to_string(&task).unwrap();
163        let back: Task = serde_json::from_str(&json).unwrap();
164        assert_eq!(back, task);
165    }
166
167    #[test]
168    fn task_optional_fields_omitted_when_none() {
169        let task = submitted_task("t1");
170        let json = serde_json::to_string(&task).unwrap();
171        assert!(!json.contains("contextId"));
172        assert!(!json.contains("artifacts"));
173        assert!(!json.contains("history"));
174        assert!(!json.contains("metadata"));
175    }
176
177    #[test]
178    fn task_json_uses_camel_case_context_id() {
179        let mut task = submitted_task("t2");
180        task.context_id = Some("ctx-1".to_string());
181        let json = serde_json::to_string(&task).unwrap();
182        assert!(json.contains("contextId"));
183        assert!(!json.contains("context_id"));
184    }
185
186    #[test]
187    fn task_with_history_roundtrip() {
188        use crate::types::Message;
189        let mut task = submitted_task("t3");
190        task.history = Some(vec![Message::user_text("hello")]);
191        let json = serde_json::to_string(&task).unwrap();
192        let back: Task = serde_json::from_str(&json).unwrap();
193        assert_eq!(back.history.as_ref().unwrap().len(), 1);
194    }
195}