Skip to main content

protocol_droid/domain/a2a/
task_lifecycle.rs

1//! A2A `GetTask` and `CancelTask` typed envelopes.
2//!
3//! # What these are
4//!
5//! [`GetTaskRequest`] and [`GetTaskResponse`] are
6//! the typed shapes for the `GetTask` JSON-RPC
7//! method. The method fetches a task's current
8//! state by id; a client polls it to advance a
9//! `Submitted` / `Working` task to a terminal
10//! state.
11//!
12//! [`CancelTaskRequest`] and
13//! [`CancelTaskResponse`] are the typed shapes
14//! for the `CancelTask` JSON-RPC method. The
15//! method requests that a task be cancelled;
16//! the response is the task as it was observed
17//! at the moment of cancellation.
18//!
19//! Both methods return a [`Task`] on success —
20//! `CancelTask` returns the task in
21//! `Cancelled` state
22//! (or the state it was in, if the task had
23//! already completed), and `GetTask` returns
24//! the task in whatever state it is currently
25//! in.
26//!
27//! # Why one file for two methods
28//!
29//! `GetTask` and `CancelTask` are paired: a
30//! client uses `GetTask` to poll a task to
31//! completion, and `CancelTask` to abort a
32//! long-running task. Their request shapes
33//! overlap (both take an id), and the
34//! file-split the project uses for the rest
35//! of the A2A IR is "one method per file",
36//! but pairing them keeps the relationship
37//! visible at a glance.
38
39use std::collections::BTreeMap;
40
41use serde::{Deserialize, Serialize};
42
43use super::task::Task;
44
45/// A typed `GetTask` request. Wire form:
46/// ```json
47/// {
48///   "id": "task-123",
49///   "historyLength": 10,
50///   "metadata": { ... }
51/// }
52/// ```
53///
54/// `id` is required; `historyLength` and
55/// `metadata` are optional. A request with
56/// only the id serialises to `{"id": "..."}`
57/// on the wire.
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub struct GetTaskRequest {
60    /// The task's id. Required.
61    pub id: String,
62    /// The number of history messages to
63    /// include in the response. A request
64    /// with no history length lets the
65    /// agent choose.
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub history_length: Option<u32>,
68    /// Per-request metadata. The spec leaves
69    /// the shape open; the conventional
70    /// keys are `"trace_id"`, `"session_id"`,
71    /// and the custom names a particular
72    /// deployment uses.
73    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
74    pub metadata: BTreeMap<String, serde_json::Value>,
75}
76
77impl GetTaskRequest {
78    /// Starts a new request with `id`. Use
79    /// [`with_history_length`](Self::with_history_length)
80    /// to set the optional history length.
81    #[must_use]
82    pub fn new(id: impl Into<String>) -> Self {
83        Self {
84            id: id.into(),
85            history_length: None,
86            metadata: BTreeMap::new(),
87        }
88    }
89
90    /// How much history to carry.
91    ///
92    /// Absent from the request unless this method is called.
93    #[must_use]
94    pub const fn with_history_length(mut self, length: u32) -> Self {
95        self.history_length = Some(length);
96        self
97    }
98}
99
100/// A typed `GetTask` response. The wire form
101/// is the [`Task`] object directly.
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103#[serde(transparent)]
104pub struct GetTaskResponse(pub Task);
105
106impl From<Task> for GetTaskResponse {
107    fn from(task: Task) -> Self {
108        Self(task)
109    }
110}
111
112impl std::ops::Deref for GetTaskResponse {
113    type Target = Task;
114
115    fn deref(&self) -> &Self::Target {
116        &self.0
117    }
118}
119
120/// A typed `CancelTask` request. Wire form:
121/// ```json
122/// { "id": "task-123", "metadata": { ... } }
123/// ```
124///
125/// `id` is required; `metadata` is optional.
126/// A request with only the id serialises to
127/// `{"id": "..."}` on the wire.
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
129pub struct CancelTaskRequest {
130    /// The task's id. Required.
131    pub id: String,
132    /// Per-request metadata.
133    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
134    pub metadata: BTreeMap<String, serde_json::Value>,
135}
136
137impl CancelTaskRequest {
138    /// Starts a new request with `id`.
139    #[must_use]
140    pub fn new(id: impl Into<String>) -> Self {
141        Self {
142            id: id.into(),
143            metadata: BTreeMap::new(),
144        }
145    }
146}
147
148/// A typed `CancelTask` response. The wire form
149/// is the [`Task`] object directly.
150///
151/// The task is whatever state it was in at the
152/// moment of cancellation — typically
153/// `Cancelled`,
154/// but possibly a terminal state if the task
155/// had already completed. The handler is
156/// responsible for setting the right state.
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
158#[serde(transparent)]
159pub struct CancelTaskResponse(pub Task);
160
161impl From<Task> for CancelTaskResponse {
162    fn from(task: Task) -> Self {
163        Self(task)
164    }
165}
166
167impl std::ops::Deref for CancelTaskResponse {
168    type Target = Task;
169
170    fn deref(&self) -> &Self::Target {
171        &self.0
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::{CancelTaskRequest, CancelTaskResponse, GetTaskRequest, GetTaskResponse};
178    use crate::domain::a2a::task::{Task, TaskState, TaskStatus};
179
180    type TestError = Box<dyn std::error::Error>;
181
182    // --- GetTaskRequest -----------------------------------------------
183
184    #[test]
185    fn get_task_request_serialises_with_id_only() -> Result<(), TestError> {
186        let request = GetTaskRequest::new("task-1");
187        let value = serde_json::to_value(&request)?;
188        let obj = value
189            .as_object()
190            .ok_or("a GetTaskRequest must serialise to a JSON object")?;
191        assert_eq!(
192            obj.get("id").and_then(serde_json::Value::as_str),
193            Some("task-1")
194        );
195        // The optional fields are absent on
196        // the wire when they are `None` /
197        // empty.
198        assert!(!obj.contains_key("historyLength"));
199        assert!(!obj.contains_key("metadata"));
200        Ok(())
201    }
202
203    #[test]
204    fn get_task_request_round_trips_with_history_length() -> Result<(), TestError> {
205        let request = GetTaskRequest::new("task-1").with_history_length(5);
206        let json = serde_json::to_string(&request)?;
207        let parsed: GetTaskRequest = serde_json::from_str(&json)?;
208        assert_eq!(parsed, request);
209        Ok(())
210    }
211
212    // --- GetTaskResponse ----------------------------------------------
213
214    #[test]
215    fn get_task_response_serialises_as_task_object() -> Result<(), TestError> {
216        let task = Task::new("task-1").with_status(TaskStatus {
217            state: TaskState::Working,
218            message: None,
219            timestamp: None,
220        });
221        let response = GetTaskResponse(task);
222        let value = serde_json::to_value(&response)?;
223        let obj = value
224            .as_object()
225            .ok_or("a GetTaskResponse must serialise to a JSON object")?;
226        // `#[serde(transparent)]` flattens the
227        // wrapper — the wire form is the
228        // inner Task, not a wrapping object.
229        assert_eq!(
230            obj.get("id").and_then(serde_json::Value::as_str),
231            Some("task-1")
232        );
233        assert!(obj.contains_key("status"));
234        Ok(())
235    }
236
237    #[test]
238    fn get_task_response_from_task_via_into_works() {
239        let task = Task::new("task-1");
240        let response: GetTaskResponse = task.clone().into();
241        assert_eq!(response.0, task);
242    }
243
244    // --- CancelTaskRequest --------------------------------------------
245
246    #[test]
247    fn cancel_task_request_serialises_with_id_only() -> Result<(), TestError> {
248        let request = CancelTaskRequest::new("task-1");
249        let value = serde_json::to_value(&request)?;
250        let obj = value
251            .as_object()
252            .ok_or("a CancelTaskRequest must serialise to a JSON object")?;
253        assert_eq!(
254            obj.get("id").and_then(serde_json::Value::as_str),
255            Some("task-1")
256        );
257        assert!(!obj.contains_key("metadata"));
258        Ok(())
259    }
260
261    #[test]
262    fn cancel_task_request_round_trips() -> Result<(), TestError> {
263        let request = CancelTaskRequest::new("task-1");
264        let json = serde_json::to_string(&request)?;
265        let parsed: CancelTaskRequest = serde_json::from_str(&json)?;
266        assert_eq!(parsed, request);
267        Ok(())
268    }
269
270    // --- CancelTaskResponse -------------------------------------------
271
272    #[test]
273    fn cancel_task_response_serialises_cancelled_state() -> Result<(), TestError> {
274        // The typical wire form: a task in
275        // `Cancelled` state. The handler
276        // is responsible for setting the
277        // right state; the response type
278        // just wraps the Task.
279        let task = Task::new("task-1").with_status(TaskStatus {
280            state: TaskState::Cancelled,
281            message: None,
282            timestamp: None,
283        });
284        let response = CancelTaskResponse(task);
285        let json = serde_json::to_string(&response)?;
286        let value: serde_json::Value = serde_json::from_str(&json)?;
287        let obj = value
288            .as_object()
289            .ok_or("a CancelTaskResponse must serialise to a JSON object")?;
290        let status = obj
291            .get("status")
292            .and_then(serde_json::Value::as_object)
293            .ok_or("`status` must be a JSON object")?;
294        assert_eq!(
295            status.get("state").and_then(serde_json::Value::as_str),
296            Some("cancelled")
297        );
298        Ok(())
299    }
300}