Skip to main content

a2a_rust/types/
responses.rs

1use serde::{Deserialize, Serialize};
2
3use crate::A2AError;
4use crate::types::JsonObject;
5
6use super::message::{Artifact, Message};
7use super::push::TaskPushNotificationConfig;
8use super::task::{Task, TaskStatus};
9
10/// Streaming event emitted when a task status changes.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct TaskStatusUpdateEvent {
14    /// Task identifier.
15    pub task_id: String,
16    /// Context identifier shared with the task.
17    pub context_id: String,
18    /// Updated task status snapshot.
19    pub status: TaskStatus,
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    /// Optional event metadata.
22    pub metadata: Option<JsonObject>,
23}
24
25/// Streaming event emitted when an artifact changes.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct TaskArtifactUpdateEvent {
29    /// Task identifier.
30    pub task_id: String,
31    /// Context identifier shared with the task.
32    pub context_id: String,
33    /// Artifact snapshot or chunk.
34    pub artifact: Artifact,
35    #[serde(default, skip_serializing_if = "crate::types::is_false")]
36    /// Whether the artifact payload should append to prior chunks.
37    pub append: bool,
38    #[serde(default, skip_serializing_if = "crate::types::is_false")]
39    /// Whether this is the last chunk for the artifact.
40    pub last_chunk: bool,
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    /// Optional event metadata.
43    pub metadata: Option<JsonObject>,
44}
45
46fn validate_task(task: &Task) -> Result<(), A2AError> {
47    for artifact in &task.artifacts {
48        artifact.validate()?;
49    }
50
51    for message in &task.history {
52        message.validate()?;
53    }
54
55    if let Some(message) = &task.status.message {
56        message.validate()?;
57    }
58
59    Ok(())
60}
61
62/// Oneof-style result for `SendMessage`.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub enum SendMessageResponse {
66    /// Response returned as a task.
67    Task(Task),
68    /// Response returned directly as a message.
69    Message(Message),
70}
71
72impl SendMessageResponse {
73    /// Validate nested task or message content.
74    pub fn validate(&self) -> Result<(), A2AError> {
75        match self {
76            Self::Task(task) => validate_task(task),
77            Self::Message(message) => message.validate(),
78        }
79    }
80}
81
82/// Oneof-style item emitted on streaming operations.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase")]
85pub enum StreamResponse {
86    /// Task snapshot event.
87    Task(Task),
88    /// Message event.
89    Message(Message),
90    /// Task status update event.
91    StatusUpdate(TaskStatusUpdateEvent),
92    /// Task artifact update event.
93    ArtifactUpdate(TaskArtifactUpdateEvent),
94}
95
96impl StreamResponse {
97    /// Validate nested event content.
98    pub fn validate(&self) -> Result<(), A2AError> {
99        match self {
100            Self::Task(task) => validate_task(task),
101            Self::Message(message) => message.validate(),
102            Self::StatusUpdate(update) => {
103                if let Some(message) = &update.status.message {
104                    message.validate()?;
105                }
106
107                Ok(())
108            }
109            Self::ArtifactUpdate(update) => update.artifact.validate(),
110        }
111    }
112}
113
114/// Paginated response for `ListTasks`.
115#[derive(Debug, Clone, Default, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase")]
117pub struct ListTasksResponse {
118    #[serde(default, skip_serializing_if = "Vec::is_empty")]
119    /// Returned task page.
120    pub tasks: Vec<Task>,
121    /// Opaque token for the next page, or an empty string when exhausted.
122    pub next_page_token: String,
123    /// Requested page size echoed in the response.
124    pub page_size: i32,
125    /// Total number of matching tasks.
126    pub total_size: i32,
127}
128
129/// Paginated response for `ListTaskPushNotificationConfigs`.
130#[derive(Debug, Clone, Default, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase")]
132pub struct ListTaskPushNotificationConfigsResponse {
133    #[serde(default, skip_serializing_if = "Vec::is_empty")]
134    /// Returned push-notification configuration page.
135    pub configs: Vec<TaskPushNotificationConfig>,
136    #[serde(default, skip_serializing_if = "String::is_empty")]
137    /// Opaque token for the next page, or an empty string when exhausted.
138    pub next_page_token: String,
139}
140
141#[cfg(test)]
142mod tests {
143    use super::{
144        ListTaskPushNotificationConfigsResponse, SendMessageResponse, StreamResponse,
145        TaskArtifactUpdateEvent, TaskStatusUpdateEvent,
146    };
147    use crate::types::{Artifact, Message, Part, Role, Task, TaskState, TaskStatus};
148
149    #[test]
150    fn send_message_response_uses_proto_oneof_shape() {
151        let response = SendMessageResponse::Message(Message {
152            message_id: "msg-1".to_owned(),
153            context_id: None,
154            task_id: None,
155            role: Role::Agent,
156            parts: vec![Part {
157                text: Some("done".to_owned()),
158                raw: None,
159                url: None,
160                data: None,
161                metadata: None,
162                filename: None,
163                media_type: None,
164            }],
165            metadata: None,
166            extensions: Vec::new(),
167            reference_task_ids: Vec::new(),
168        });
169
170        let json = serde_json::to_string(&response).expect("response should serialize");
171        assert_eq!(
172            json,
173            r#"{"message":{"messageId":"msg-1","role":"ROLE_AGENT","parts":[{"text":"done"}]}}"#
174        );
175    }
176
177    #[test]
178    fn send_message_response_validate_rejects_invalid_part() {
179        let response = SendMessageResponse::Message(Message {
180            message_id: "msg-1".to_owned(),
181            context_id: None,
182            task_id: None,
183            role: Role::Agent,
184            parts: vec![Part {
185                text: Some("done".to_owned()),
186                raw: Some(vec![104, 105]),
187                url: None,
188                data: None,
189                metadata: None,
190                filename: None,
191                media_type: None,
192            }],
193            metadata: None,
194            extensions: Vec::new(),
195            reference_task_ids: Vec::new(),
196        });
197
198        let error = response.validate().expect_err("response should be invalid");
199        assert!(
200            error
201                .to_string()
202                .contains("part cannot contain more than one")
203        );
204    }
205
206    #[test]
207    fn list_push_notification_response_uses_empty_string_for_no_next_page() {
208        let response = ListTaskPushNotificationConfigsResponse {
209            configs: Vec::new(),
210            next_page_token: String::new(),
211        };
212
213        let json = serde_json::to_string(&response).expect("response should serialize");
214        assert_eq!(json, "{}");
215    }
216
217    #[test]
218    fn task_status_update_event_round_trip_serialization() {
219        let event = TaskStatusUpdateEvent {
220            task_id: "task-1".to_owned(),
221            context_id: "ctx-1".to_owned(),
222            status: TaskStatus {
223                state: TaskState::Working,
224                message: Some(Message {
225                    message_id: "msg-1".to_owned(),
226                    context_id: Some("ctx-1".to_owned()),
227                    task_id: Some("task-1".to_owned()),
228                    role: Role::Agent,
229                    parts: vec![Part {
230                        text: Some("still working".to_owned()),
231                        raw: None,
232                        url: None,
233                        data: None,
234                        metadata: None,
235                        filename: None,
236                        media_type: None,
237                    }],
238                    metadata: None,
239                    extensions: Vec::new(),
240                    reference_task_ids: Vec::new(),
241                }),
242                timestamp: Some("2026-03-12T12:00:00Z".to_owned()),
243            },
244            metadata: None,
245        };
246
247        let json = serde_json::to_string(&event).expect("event should serialize");
248        let round_trip: TaskStatusUpdateEvent =
249            serde_json::from_str(&json).expect("event should deserialize");
250
251        assert_eq!(round_trip.task_id, "task-1");
252        assert_eq!(round_trip.status.state, TaskState::Working);
253    }
254
255    #[test]
256    fn task_artifact_update_event_round_trip_serialization() {
257        let event = TaskArtifactUpdateEvent {
258            task_id: "task-1".to_owned(),
259            context_id: "ctx-1".to_owned(),
260            artifact: Artifact {
261                artifact_id: "artifact-1".to_owned(),
262                name: Some("result".to_owned()),
263                description: None,
264                parts: vec![Part {
265                    text: Some("partial".to_owned()),
266                    raw: None,
267                    url: None,
268                    data: None,
269                    metadata: None,
270                    filename: None,
271                    media_type: None,
272                }],
273                metadata: None,
274                extensions: Vec::new(),
275            },
276            append: true,
277            last_chunk: false,
278            metadata: None,
279        };
280
281        let json = serde_json::to_string(&event).expect("event should serialize");
282        let round_trip: TaskArtifactUpdateEvent =
283            serde_json::from_str(&json).expect("event should deserialize");
284
285        assert!(round_trip.append);
286        assert!(!round_trip.last_chunk);
287        assert_eq!(round_trip.artifact.artifact_id, "artifact-1");
288    }
289
290    #[test]
291    fn stream_response_round_trip_serialization() {
292        let response = StreamResponse::Task(Task {
293            id: "task-1".to_owned(),
294            context_id: Some("ctx-1".to_owned()),
295            status: TaskStatus {
296                state: TaskState::Submitted,
297                message: None,
298                timestamp: Some("2026-03-12T12:00:00Z".to_owned()),
299            },
300            artifacts: Vec::new(),
301            history: Vec::new(),
302            metadata: None,
303        });
304
305        let json = serde_json::to_string(&response).expect("response should serialize");
306        let round_trip: StreamResponse =
307            serde_json::from_str(&json).expect("response should deserialize");
308
309        match round_trip {
310            StreamResponse::Task(task) => assert_eq!(task.id, "task-1"),
311            _ => panic!("expected task stream response"),
312        }
313    }
314}