Skip to main content

a2a/
types.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// Copyright A2A Contributors (https://github.com/a2aproject)
3// SPDX-License-Identifier: Apache-2.0
4use base64::Engine;
5use base64::engine::general_purpose::STANDARD as BASE64;
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8use serde_json::Value;
9use std::collections::HashMap;
10use uuid::Uuid;
11
12/// Support for the `fuzzing` feature: kept beside the types it services so
13/// the two evolve together, rather than in a separate crate module.
14#[cfg(feature = "fuzzing")]
15mod fuzz_support {
16    use arbitrary::{Arbitrary, Result, Unstructured};
17    use chrono::{DateTime, NaiveDate, Utc};
18
19    /// Generates a timestamp within `google.protobuf.Timestamp`'s own
20    /// documented valid range (0001-01-01T00:00:00Z to
21    /// 9999-12-31T23:59:59.999999999Z) rather than `chrono`'s much wider one.
22    ///
23    /// Outside that range, `a2a-pb`'s `to_proto_timestamp` previously produced
24    /// well-known-type JSON that could not be parsed back (the #261 round-trip
25    /// bug, now fixed: it returns `None`). Generating only in-range values here
26    /// keeps fuzz target 2 pointed at timestamps a real `Task` could actually
27    /// carry, rather than at values whose behaviour #261 already pins.
28    pub(crate) fn bounded_timestamp(u: &mut Unstructured) -> Result<Option<DateTime<Utc>>> {
29        if !bool::arbitrary(u)? {
30            return Ok(None);
31        }
32        let min = NaiveDate::from_ymd_opt(1, 1, 1)
33            .unwrap()
34            .and_hms_opt(0, 0, 0)
35            .unwrap()
36            .and_utc();
37        let max = NaiveDate::from_ymd_opt(9999, 12, 31)
38            .unwrap()
39            .and_hms_nano_opt(23, 59, 59, 999_999_999)
40            .unwrap()
41            .and_utc();
42        let secs = u.int_in_range(min.timestamp()..=max.timestamp())?;
43        let nanos = u.int_in_range(0u32..=999_999_999u32)?;
44        Ok(DateTime::from_timestamp(secs, nanos))
45    }
46}
47
48// ---------------------------------------------------------------------------
49// TaskID / ArtifactID / ContextID
50// ---------------------------------------------------------------------------
51
52/// Unique identifier for a task, generated by the server.
53pub type TaskId = String;
54
55/// Unique identifier for an artifact within a task.
56pub type ArtifactId = String;
57
58/// Generate a new random task ID (UUIDv7).
59pub fn new_task_id() -> TaskId {
60    Uuid::now_v7().to_string()
61}
62
63/// Generate a new random context ID (UUIDv7).
64pub fn new_context_id() -> String {
65    Uuid::now_v7().to_string()
66}
67
68/// Generate a new random message ID (UUIDv7).
69pub fn new_message_id() -> String {
70    Uuid::now_v7().to_string()
71}
72
73/// Generate a new random artifact ID (UUIDv7).
74pub fn new_artifact_id() -> ArtifactId {
75    Uuid::now_v7().to_string()
76}
77
78// ---------------------------------------------------------------------------
79// MessageRole
80// ---------------------------------------------------------------------------
81
82/// Identifies the sender of a message.
83#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
84#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
85pub enum Role {
86    #[default]
87    Unspecified,
88    User,
89    Agent,
90}
91
92impl Serialize for Role {
93    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
94        match self {
95            Role::Unspecified => serializer.serialize_str("ROLE_UNSPECIFIED"),
96            Role::User => serializer.serialize_str("ROLE_USER"),
97            Role::Agent => serializer.serialize_str("ROLE_AGENT"),
98        }
99    }
100}
101
102impl<'de> Deserialize<'de> for Role {
103    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
104        let s = String::deserialize(deserializer)?;
105        match s.as_str() {
106            "ROLE_USER" => Ok(Role::User),
107            "ROLE_AGENT" => Ok(Role::Agent),
108            "ROLE_UNSPECIFIED" | "" => Ok(Role::Unspecified),
109            other => Err(serde::de::Error::unknown_variant(
110                other,
111                &["ROLE_USER", "ROLE_AGENT", "ROLE_UNSPECIFIED"],
112            )),
113        }
114    }
115}
116
117// ---------------------------------------------------------------------------
118// TaskState
119// ---------------------------------------------------------------------------
120
121/// The lifecycle state of a task.
122#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
123#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
124pub enum TaskState {
125    #[default]
126    Unspecified,
127    Submitted,
128    Working,
129    Completed,
130    Failed,
131    Canceled,
132    InputRequired,
133    Rejected,
134    AuthRequired,
135}
136
137impl TaskState {
138    /// Returns true for terminal states where no further changes are permitted.
139    pub fn is_terminal(&self) -> bool {
140        matches!(
141            self,
142            TaskState::Completed | TaskState::Failed | TaskState::Canceled | TaskState::Rejected
143        )
144    }
145}
146
147impl Serialize for TaskState {
148    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
149        let s = match self {
150            TaskState::Unspecified => "TASK_STATE_UNSPECIFIED",
151            TaskState::Submitted => "TASK_STATE_SUBMITTED",
152            TaskState::Working => "TASK_STATE_WORKING",
153            TaskState::Completed => "TASK_STATE_COMPLETED",
154            TaskState::Failed => "TASK_STATE_FAILED",
155            TaskState::Canceled => "TASK_STATE_CANCELED",
156            TaskState::InputRequired => "TASK_STATE_INPUT_REQUIRED",
157            TaskState::Rejected => "TASK_STATE_REJECTED",
158            TaskState::AuthRequired => "TASK_STATE_AUTH_REQUIRED",
159        };
160        serializer.serialize_str(s)
161    }
162}
163
164impl<'de> Deserialize<'de> for TaskState {
165    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
166        let s = String::deserialize(deserializer)?;
167        match s.as_str() {
168            "TASK_STATE_SUBMITTED" => Ok(TaskState::Submitted),
169            "TASK_STATE_WORKING" => Ok(TaskState::Working),
170            "TASK_STATE_COMPLETED" => Ok(TaskState::Completed),
171            "TASK_STATE_FAILED" => Ok(TaskState::Failed),
172            "TASK_STATE_CANCELED" => Ok(TaskState::Canceled),
173            "TASK_STATE_INPUT_REQUIRED" => Ok(TaskState::InputRequired),
174            "TASK_STATE_REJECTED" => Ok(TaskState::Rejected),
175            "TASK_STATE_AUTH_REQUIRED" => Ok(TaskState::AuthRequired),
176            "TASK_STATE_UNSPECIFIED" | "" => Ok(TaskState::Unspecified),
177            other => Err(serde::de::Error::unknown_variant(
178                other,
179                &[
180                    "TASK_STATE_SUBMITTED",
181                    "TASK_STATE_WORKING",
182                    "TASK_STATE_COMPLETED",
183                    "TASK_STATE_FAILED",
184                    "TASK_STATE_CANCELED",
185                    "TASK_STATE_INPUT_REQUIRED",
186                    "TASK_STATE_REJECTED",
187                    "TASK_STATE_AUTH_REQUIRED",
188                    "TASK_STATE_UNSPECIFIED",
189                ],
190            )),
191        }
192    }
193}
194
195// ---------------------------------------------------------------------------
196// Part (field-presence union)
197// ---------------------------------------------------------------------------
198
199/// A content part of a message or artifact. Uses field-presence serialization
200/// (like C# SDK) so only the relevant content field is present in JSON.
201#[derive(Debug, Clone, PartialEq)]
202#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
203pub struct Part {
204    pub content: PartContent,
205    #[doc = "Optional filename for file parts."]
206    pub filename: Option<String>,
207    #[doc = "MIME type of the part content."]
208    pub media_type: Option<String>,
209    #[doc = "Optional metadata for extensions."]
210    #[cfg_attr(feature = "fuzzing", arbitrary(default))]
211    pub metadata: Option<HashMap<String, Value>>,
212}
213
214/// The content of a part — a discriminated union.
215#[derive(Debug, Clone, PartialEq)]
216#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
217pub enum PartContent {
218    Text(String),
219    Raw(Vec<u8>),
220    Url(String),
221    Data(#[cfg_attr(feature = "fuzzing", arbitrary(default))] Value),
222}
223
224impl Part {
225    pub fn text(text: impl Into<String>) -> Self {
226        Part {
227            content: PartContent::Text(text.into()),
228            filename: None,
229            media_type: None,
230            metadata: None,
231        }
232    }
233
234    pub fn raw(data: Vec<u8>) -> Self {
235        Part {
236            content: PartContent::Raw(data),
237            filename: None,
238            media_type: None,
239            metadata: None,
240        }
241    }
242
243    pub fn url(url: impl Into<String>) -> Self {
244        Part {
245            content: PartContent::Url(url.into()),
246            filename: None,
247            media_type: None,
248            metadata: None,
249        }
250    }
251
252    pub fn data(value: Value) -> Self {
253        Part {
254            content: PartContent::Data(value),
255            filename: None,
256            media_type: None,
257            metadata: None,
258        }
259    }
260
261    pub fn with_media_type(mut self, media_type: impl Into<String>) -> Self {
262        self.media_type = Some(media_type.into());
263        self
264    }
265
266    pub fn with_filename(mut self, filename: impl Into<String>) -> Self {
267        self.filename = Some(filename.into());
268        self
269    }
270
271    pub fn as_text(&self) -> Option<&str> {
272        if let PartContent::Text(ref s) = self.content {
273            Some(s)
274        } else {
275            None
276        }
277    }
278}
279
280impl Serialize for Part {
281    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
282        use serde::ser::SerializeMap;
283        let mut map = serializer.serialize_map(None)?;
284
285        match &self.content {
286            PartContent::Text(t) => map.serialize_entry("text", t)?,
287            PartContent::Raw(r) => map.serialize_entry("raw", &BASE64.encode(r))?,
288            PartContent::Url(u) => map.serialize_entry("url", u)?,
289            PartContent::Data(d) => map.serialize_entry("data", d)?,
290        }
291
292        if let Some(ref f) = self.filename {
293            map.serialize_entry("filename", f)?;
294        }
295        if let Some(ref m) = self.media_type {
296            map.serialize_entry("mediaType", m)?;
297        }
298        if let Some(ref meta) = self.metadata {
299            if !meta.is_empty() {
300                map.serialize_entry("metadata", meta)?;
301            }
302        }
303        map.end()
304    }
305}
306
307impl<'de> Deserialize<'de> for Part {
308    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
309        let raw: HashMap<String, Value> = HashMap::deserialize(deserializer)?;
310
311        let content = if let Some(Value::String(t)) = raw.get("text") {
312            PartContent::Text(t.clone())
313        } else if let Some(Value::String(r)) = raw.get("raw") {
314            let bytes = BASE64.decode(r).map_err(serde::de::Error::custom)?;
315            PartContent::Raw(bytes)
316        } else if let Some(Value::String(u)) = raw.get("url") {
317            PartContent::Url(u.clone())
318        } else if let Some(d) = raw.get("data") {
319            PartContent::Data(d.clone())
320        } else {
321            return Err(serde::de::Error::custom(
322                "Part must have one of: text, raw, url, data",
323            ));
324        };
325
326        let filename = raw
327            .get("filename")
328            .and_then(|v| v.as_str())
329            .map(String::from);
330        let media_type = raw
331            .get("mediaType")
332            .and_then(|v| v.as_str())
333            .map(String::from);
334        let metadata: Option<HashMap<String, Value>> = raw
335            .get("metadata")
336            .and_then(|v| serde_json::from_value(v.clone()).ok());
337
338        Ok(Part {
339            content,
340            filename,
341            media_type,
342            metadata,
343        })
344    }
345}
346
347// ---------------------------------------------------------------------------
348// Message
349// ---------------------------------------------------------------------------
350
351/// A single message in a conversation between user and agent.
352#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
353#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
354#[serde(rename_all = "camelCase")]
355pub struct Message {
356    /// Unique identifier for the message, generated by the sender.
357    pub message_id: String,
358
359    /// Context identifier for grouping related interactions.
360    #[serde(default, skip_serializing_if = "Option::is_none")]
361    pub context_id: Option<String>,
362
363    /// Task identifier this message belongs to.
364    #[serde(default, skip_serializing_if = "Option::is_none")]
365    pub task_id: Option<TaskId>,
366
367    /// Sender role.
368    pub role: Role,
369
370    /// Content parts forming the message body.
371    pub parts: Vec<Part>,
372
373    /// Optional metadata for extensions.
374    #[serde(default, skip_serializing_if = "Option::is_none")]
375    #[cfg_attr(feature = "fuzzing", arbitrary(default))]
376    pub metadata: Option<HashMap<String, Value>>,
377
378    /// Extension URIs relevant to this message.
379    #[serde(default, skip_serializing_if = "Option::is_none")]
380    pub extensions: Option<Vec<String>>,
381
382    /// Related task IDs for additional context.
383    #[serde(default, skip_serializing_if = "Option::is_none")]
384    pub reference_task_ids: Option<Vec<TaskId>>,
385}
386
387impl Message {
388    /// Create a new message with a random ID.
389    pub fn new(role: Role, parts: Vec<Part>) -> Self {
390        Message {
391            message_id: new_message_id(),
392            context_id: None,
393            task_id: None,
394            role,
395            parts,
396            metadata: None,
397            extensions: None,
398            reference_task_ids: None,
399        }
400    }
401
402    /// Return the text of the first text part, if any.
403    pub fn text(&self) -> Option<&str> {
404        self.parts.iter().find_map(|p| p.as_text())
405    }
406}
407
408// ---------------------------------------------------------------------------
409// TaskStatus
410// ---------------------------------------------------------------------------
411
412/// The status of a task at a specific point in time.
413#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
414#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
415#[serde(rename_all = "camelCase")]
416pub struct TaskStatus {
417    pub state: TaskState,
418
419    #[serde(default, skip_serializing_if = "Option::is_none")]
420    pub message: Option<Message>,
421
422    #[serde(default, skip_serializing_if = "Option::is_none")]
423    #[cfg_attr(feature = "fuzzing", arbitrary(with = fuzz_support::bounded_timestamp))]
424    pub timestamp: Option<DateTime<Utc>>,
425}
426
427// ---------------------------------------------------------------------------
428// Task
429// ---------------------------------------------------------------------------
430
431/// A single stateful operation or conversation between client and agent.
432#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
433#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
434#[serde(rename_all = "camelCase")]
435pub struct Task {
436    pub id: TaskId,
437    pub context_id: String,
438    pub status: TaskStatus,
439
440    #[serde(default, skip_serializing_if = "Option::is_none")]
441    pub artifacts: Option<Vec<Artifact>>,
442
443    #[serde(default, skip_serializing_if = "Option::is_none")]
444    pub history: Option<Vec<Message>>,
445
446    #[serde(default, skip_serializing_if = "Option::is_none")]
447    #[cfg_attr(feature = "fuzzing", arbitrary(default))]
448    pub metadata: Option<HashMap<String, Value>>,
449}
450
451// ---------------------------------------------------------------------------
452// Artifact
453// ---------------------------------------------------------------------------
454
455/// A resource generated by an agent during a task.
456#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
457#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
458#[serde(rename_all = "camelCase")]
459pub struct Artifact {
460    pub artifact_id: ArtifactId,
461
462    #[serde(default, skip_serializing_if = "Option::is_none")]
463    pub name: Option<String>,
464
465    #[serde(default, skip_serializing_if = "Option::is_none")]
466    pub description: Option<String>,
467
468    pub parts: Vec<Part>,
469
470    #[serde(default, skip_serializing_if = "Option::is_none")]
471    #[cfg_attr(feature = "fuzzing", arbitrary(default))]
472    pub metadata: Option<HashMap<String, Value>>,
473
474    #[serde(default, skip_serializing_if = "Option::is_none")]
475    pub extensions: Option<Vec<String>>,
476}
477
478// ---------------------------------------------------------------------------
479// Request / Response types
480// ---------------------------------------------------------------------------
481
482/// Configuration for `SendMessage` / `SendStreamingMessage`.
483#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
484#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
485#[serde(rename_all = "camelCase")]
486pub struct SendMessageConfiguration {
487    #[serde(default, skip_serializing_if = "Option::is_none")]
488    pub accepted_output_modes: Option<Vec<String>>,
489
490    #[serde(default, skip_serializing_if = "Option::is_none")]
491    #[serde(alias = "pushNotificationConfig")]
492    pub task_push_notification_config: Option<TaskPushNotificationConfig>,
493
494    #[serde(default, skip_serializing_if = "Option::is_none")]
495    pub history_length: Option<i32>,
496
497    #[serde(default, skip_serializing_if = "Option::is_none")]
498    pub return_immediately: Option<bool>,
499}
500
501/// Request to send a message to an agent.
502#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
503#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
504#[serde(rename_all = "camelCase")]
505pub struct SendMessageRequest {
506    pub message: Message,
507
508    #[serde(default, skip_serializing_if = "Option::is_none")]
509    pub configuration: Option<SendMessageConfiguration>,
510
511    #[serde(default, skip_serializing_if = "Option::is_none")]
512    #[cfg_attr(feature = "fuzzing", arbitrary(default))]
513    pub metadata: Option<HashMap<String, Value>>,
514
515    #[serde(default, skip_serializing_if = "Option::is_none")]
516    pub tenant: Option<String>,
517}
518
519/// Response from `SendMessage` — either a Task or a Message.
520#[derive(Debug, Clone, PartialEq)]
521#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
522pub enum SendMessageResponse {
523    Task(Task),
524    Message(Message),
525}
526
527impl Serialize for SendMessageResponse {
528    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
529        use serde::ser::SerializeMap;
530        let mut map = serializer.serialize_map(Some(1))?;
531        match self {
532            SendMessageResponse::Task(t) => map.serialize_entry("task", t)?,
533            SendMessageResponse::Message(m) => map.serialize_entry("message", m)?,
534        }
535        map.end()
536    }
537}
538
539impl<'de> Deserialize<'de> for SendMessageResponse {
540    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
541        let raw: HashMap<String, Value> = HashMap::deserialize(deserializer)?;
542        if let Some(v) = raw.get("task") {
543            let task: Task = serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?;
544            Ok(SendMessageResponse::Task(task))
545        } else if let Some(v) = raw.get("message") {
546            let msg: Message =
547                serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?;
548            Ok(SendMessageResponse::Message(msg))
549        } else {
550            Err(serde::de::Error::custom(
551                "SendMessageResponse must have 'task' or 'message'",
552            ))
553        }
554    }
555}
556
557/// Request to get a task by ID.
558#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
559#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
560#[serde(rename_all = "camelCase")]
561pub struct GetTaskRequest {
562    pub id: TaskId,
563
564    #[serde(default, skip_serializing_if = "Option::is_none")]
565    pub history_length: Option<i32>,
566
567    #[serde(default, skip_serializing_if = "Option::is_none")]
568    pub tenant: Option<String>,
569}
570
571/// Request to list tasks.
572#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
573#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
574#[serde(rename_all = "camelCase")]
575pub struct ListTasksRequest {
576    #[serde(default, skip_serializing_if = "Option::is_none")]
577    pub context_id: Option<String>,
578
579    #[serde(default, skip_serializing_if = "Option::is_none")]
580    pub status: Option<TaskState>,
581
582    #[serde(default, skip_serializing_if = "Option::is_none")]
583    pub page_size: Option<i32>,
584
585    #[serde(default, skip_serializing_if = "Option::is_none")]
586    pub page_token: Option<String>,
587
588    #[serde(default, skip_serializing_if = "Option::is_none")]
589    pub history_length: Option<i32>,
590
591    #[serde(default, skip_serializing_if = "Option::is_none")]
592    #[cfg_attr(feature = "fuzzing", arbitrary(with = fuzz_support::bounded_timestamp))]
593    pub status_timestamp_after: Option<DateTime<Utc>>,
594
595    #[serde(default, skip_serializing_if = "Option::is_none")]
596    pub include_artifacts: Option<bool>,
597
598    #[serde(default, skip_serializing_if = "Option::is_none")]
599    pub tenant: Option<String>,
600}
601
602/// Response for listing tasks.
603#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
604#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
605#[serde(rename_all = "camelCase")]
606pub struct ListTasksResponse {
607    pub tasks: Vec<Task>,
608    pub next_page_token: String,
609    pub page_size: i32,
610    pub total_size: i32,
611}
612
613/// Request to cancel a task.
614#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
615#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
616#[serde(rename_all = "camelCase")]
617pub struct CancelTaskRequest {
618    pub id: TaskId,
619
620    #[serde(default, skip_serializing_if = "Option::is_none")]
621    #[cfg_attr(feature = "fuzzing", arbitrary(default))]
622    pub metadata: Option<HashMap<String, Value>>,
623
624    #[serde(default, skip_serializing_if = "Option::is_none")]
625    pub tenant: Option<String>,
626}
627
628/// Request to subscribe to task updates.
629#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
630#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
631#[serde(rename_all = "camelCase")]
632pub struct SubscribeToTaskRequest {
633    pub id: TaskId,
634
635    #[serde(default, skip_serializing_if = "Option::is_none")]
636    pub tenant: Option<String>,
637}
638
639/// Request to get an extended (authenticated) agent card.
640#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
641#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
642#[serde(rename_all = "camelCase")]
643pub struct GetExtendedAgentCardRequest {
644    #[serde(default, skip_serializing_if = "Option::is_none")]
645    pub tenant: Option<String>,
646}
647
648// ---------------------------------------------------------------------------
649// Push Notification types
650// ---------------------------------------------------------------------------
651
652/// Authentication details for a push notification endpoint.
653#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
654#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
655#[serde(rename_all = "camelCase")]
656pub struct AuthenticationInfo {
657    /// HTTP authentication scheme (e.g., "Bearer", "Basic").
658    pub scheme: String,
659
660    /// Scheme-specific credentials.
661    #[serde(default, skip_serializing_if = "Option::is_none")]
662    pub credentials: Option<String>,
663}
664
665/// Push notification configuration for a task.
666#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
667#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
668#[serde(rename_all = "camelCase")]
669pub struct TaskPushNotificationConfig {
670    /// Callback URL for push notifications.
671    pub url: String,
672
673    /// Optional unique ID for this configuration.
674    #[serde(default, skip_serializing_if = "Option::is_none")]
675    pub id: Option<String>,
676
677    /// The task this configuration is associated with.
678    #[serde(default, skip_serializing_if = "String::is_empty")]
679    pub task_id: TaskId,
680
681    /// Optional session/task token for validation.
682    #[serde(default, skip_serializing_if = "Option::is_none")]
683    pub token: Option<String>,
684
685    /// Optional authentication details for the webhook.
686    #[serde(default, skip_serializing_if = "Option::is_none")]
687    pub authentication: Option<AuthenticationInfo>,
688
689    #[serde(default, skip_serializing_if = "Option::is_none")]
690    pub tenant: Option<String>,
691}
692
693/// Request to get a specific push config.
694#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
695#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
696#[serde(rename_all = "camelCase")]
697pub struct GetTaskPushNotificationConfigRequest {
698    pub task_id: TaskId,
699    pub id: String,
700
701    #[serde(default, skip_serializing_if = "Option::is_none")]
702    pub tenant: Option<String>,
703}
704
705/// Request to list push configs for a task.
706#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
707#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
708#[serde(rename_all = "camelCase")]
709pub struct ListTaskPushNotificationConfigsRequest {
710    pub task_id: TaskId,
711
712    #[serde(default, skip_serializing_if = "Option::is_none")]
713    pub page_size: Option<i32>,
714
715    #[serde(default, skip_serializing_if = "Option::is_none")]
716    pub page_token: Option<String>,
717
718    #[serde(default, skip_serializing_if = "Option::is_none")]
719    pub tenant: Option<String>,
720}
721
722/// Response for listing push configs.
723#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
724#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
725#[serde(rename_all = "camelCase")]
726pub struct ListTaskPushNotificationConfigsResponse {
727    #[serde(default, skip_serializing_if = "Vec::is_empty")]
728    pub configs: Vec<TaskPushNotificationConfig>,
729
730    #[serde(default, skip_serializing_if = "Option::is_none")]
731    pub next_page_token: Option<String>,
732}
733
734/// Request to delete a push config.
735#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
736#[cfg_attr(feature = "fuzzing", derive(arbitrary::Arbitrary))]
737#[serde(rename_all = "camelCase")]
738pub struct DeleteTaskPushNotificationConfigRequest {
739    pub task_id: TaskId,
740    pub id: String,
741
742    #[serde(default, skip_serializing_if = "Option::is_none")]
743    pub tenant: Option<String>,
744}
745
746// ---------------------------------------------------------------------------
747// TransportProtocol
748// ---------------------------------------------------------------------------
749
750/// Transport protocol identifier. Open-form string to allow custom protocols.
751pub type TransportProtocol = String;
752
753/// JSON-RPC transport protocol constant.
754pub const TRANSPORT_PROTOCOL_JSONRPC: &str = "JSONRPC";
755/// gRPC transport protocol constant.
756pub const TRANSPORT_PROTOCOL_GRPC: &str = "GRPC";
757/// HTTP+JSON (REST) transport protocol constant.
758pub const TRANSPORT_PROTOCOL_HTTP_JSON: &str = "HTTP+JSON";
759/// SLIMRPC transport protocol constant.
760pub const TRANSPORT_PROTOCOL_SLIMRPC: &str = "SLIMRPC";
761
762/// Protocol version string (e.g., "1.0").
763pub type ProtocolVersion = String;
764
765#[cfg(test)]
766mod tests {
767    use super::*;
768
769    #[test]
770    fn test_task_state_serde() {
771        let state = TaskState::Completed;
772        let json = serde_json::to_string(&state).unwrap();
773        assert_eq!(json, r#""TASK_STATE_COMPLETED""#);
774        let back: TaskState = serde_json::from_str(&json).unwrap();
775        assert_eq!(back, TaskState::Completed);
776    }
777
778    #[test]
779    fn test_role_serde() {
780        let role = Role::Agent;
781        let json = serde_json::to_string(&role).unwrap();
782        assert_eq!(json, r#""ROLE_AGENT""#);
783        let back: Role = serde_json::from_str(&json).unwrap();
784        assert_eq!(back, Role::Agent);
785    }
786
787    #[test]
788    fn test_part_text_serde() {
789        let part = Part::text("hello");
790        let json = serde_json::to_string(&part).unwrap();
791        let v: Value = serde_json::from_str(&json).unwrap();
792        assert_eq!(v["text"], "hello");
793        assert!(v.get("raw").is_none());
794
795        let back: Part = serde_json::from_str(&json).unwrap();
796        assert_eq!(back.content, PartContent::Text("hello".into()));
797    }
798
799    #[test]
800    fn test_part_raw_serde() {
801        let part = Part::raw(vec![1, 2, 3]);
802        let json = serde_json::to_string(&part).unwrap();
803        let back: Part = serde_json::from_str(&json).unwrap();
804        assert_eq!(back.content, PartContent::Raw(vec![1, 2, 3]));
805    }
806
807    #[test]
808    fn test_send_message_response_serde() {
809        let resp = SendMessageResponse::Task(Task {
810            id: "t1".into(),
811            context_id: "c1".into(),
812            status: TaskStatus {
813                state: TaskState::Submitted,
814                message: None,
815                timestamp: None,
816            },
817            artifacts: None,
818            history: None,
819            metadata: None,
820        });
821        let json = serde_json::to_string(&resp).unwrap();
822        let v: Value = serde_json::from_str(&json).unwrap();
823        assert!(v.get("task").is_some());
824        assert!(v.get("message").is_none());
825        let back: SendMessageResponse = serde_json::from_str(&json).unwrap();
826        assert!(matches!(back, SendMessageResponse::Task(_)));
827    }
828
829    #[test]
830    fn test_send_message_response_message_variant() {
831        let resp = SendMessageResponse::Message(Message::new(Role::Agent, vec![Part::text("hi")]));
832        let json = serde_json::to_string(&resp).unwrap();
833        let v: Value = serde_json::from_str(&json).unwrap();
834        assert!(v.get("message").is_some());
835        assert!(v.get("task").is_none());
836        let back: SendMessageResponse = serde_json::from_str(&json).unwrap();
837        assert!(matches!(back, SendMessageResponse::Message(_)));
838    }
839
840    #[test]
841    fn test_part_url_serde() {
842        let part = Part::url("https://example.com/file.pdf");
843        let json = serde_json::to_string(&part).unwrap();
844        let v: Value = serde_json::from_str(&json).unwrap();
845        assert_eq!(v["url"], "https://example.com/file.pdf");
846        let back: Part = serde_json::from_str(&json).unwrap();
847        assert!(matches!(back.content, PartContent::Url(_)));
848    }
849
850    #[test]
851    fn test_part_data_serde() {
852        let data = serde_json::json!({"key": "value", "count": 42});
853        let part = Part::data(data.clone());
854        let json = serde_json::to_string(&part).unwrap();
855        let back: Part = serde_json::from_str(&json).unwrap();
856        assert!(matches!(back.content, PartContent::Data(_)));
857        if let PartContent::Data(d) = back.content {
858            assert_eq!(d["key"], "value");
859        }
860    }
861
862    #[test]
863    fn test_part_with_metadata_and_media_type() {
864        let part = Part::text("hello")
865            .with_media_type("text/plain")
866            .with_filename("test.txt");
867        assert_eq!(part.media_type.as_deref(), Some("text/plain"));
868        assert_eq!(part.filename.as_deref(), Some("test.txt"));
869        let json = serde_json::to_string(&part).unwrap();
870        let v: Value = serde_json::from_str(&json).unwrap();
871        assert_eq!(v["mediaType"], "text/plain");
872        assert_eq!(v["filename"], "test.txt");
873    }
874
875    #[test]
876    fn test_part_as_text() {
877        let part = Part::text("hello");
878        assert_eq!(part.as_text(), Some("hello"));
879        let part = Part::raw(vec![]);
880        assert_eq!(part.as_text(), None);
881    }
882
883    #[test]
884    fn test_message_new() {
885        let msg = Message::new(Role::User, vec![Part::text("hi")]);
886        assert!(!msg.message_id.is_empty());
887        assert_eq!(msg.role, Role::User);
888        assert_eq!(msg.parts.len(), 1);
889        assert_eq!(msg.text(), Some("hi"));
890    }
891
892    #[test]
893    fn test_message_serde() {
894        let msg = Message {
895            message_id: "m1".to_string(),
896            context_id: Some("c1".to_string()),
897            task_id: None,
898            role: Role::Agent,
899            parts: vec![Part::text("response")],
900            metadata: None,
901            extensions: Some(vec!["ext1".to_string()]),
902            reference_task_ids: Some(vec!["t1".to_string()]),
903        };
904        let json = serde_json::to_string(&msg).unwrap();
905        let back: Message = serde_json::from_str(&json).unwrap();
906        assert_eq!(msg, back);
907    }
908
909    #[test]
910    fn test_task_state_is_terminal() {
911        assert!(TaskState::Completed.is_terminal());
912        assert!(TaskState::Failed.is_terminal());
913        assert!(TaskState::Canceled.is_terminal());
914        assert!(TaskState::Rejected.is_terminal());
915        assert!(!TaskState::Submitted.is_terminal());
916        assert!(!TaskState::Working.is_terminal());
917        assert!(!TaskState::InputRequired.is_terminal());
918        assert!(!TaskState::AuthRequired.is_terminal());
919        assert!(!TaskState::Unspecified.is_terminal());
920    }
921
922    #[test]
923    fn test_task_full_serde() {
924        let task = Task {
925            id: "t1".to_string(),
926            context_id: "c1".to_string(),
927            status: TaskStatus {
928                state: TaskState::Working,
929                message: Some(Message::new(Role::Agent, vec![Part::text("working")])),
930                timestamp: None,
931            },
932            artifacts: Some(vec![Artifact {
933                artifact_id: "a1".to_string(),
934                name: Some("output".to_string()),
935                description: None,
936                parts: vec![Part::text("result data")],
937                metadata: None,
938                extensions: None,
939            }]),
940            history: Some(vec![Message::new(Role::User, vec![Part::text("do it")])]),
941            metadata: None,
942        };
943        let json = serde_json::to_string(&task).unwrap();
944        let back: Task = serde_json::from_str(&json).unwrap();
945        assert_eq!(task.id, back.id);
946        assert_eq!(task.status.state, back.status.state);
947        assert!(back.artifacts.is_some());
948        assert!(back.history.is_some());
949    }
950
951    #[test]
952    fn test_push_notification_config_serde() {
953        let config = TaskPushNotificationConfig {
954            url: "https://example.com/webhook".to_string(),
955            id: Some("cfg-1".to_string()),
956            task_id: "task-1".to_string(),
957            token: Some("tok-1".to_string()),
958            authentication: Some(AuthenticationInfo {
959                scheme: "Bearer".to_string(),
960                credentials: Some("secret".to_string()),
961            }),
962            tenant: None,
963        };
964        let json = serde_json::to_string(&config).unwrap();
965        let back: TaskPushNotificationConfig = serde_json::from_str(&json).unwrap();
966        assert_eq!(config, back);
967    }
968
969    #[test]
970    fn test_send_message_request_serde() {
971        let req = SendMessageRequest {
972            message: Message::new(Role::User, vec![Part::text("hello")]),
973            configuration: Some(SendMessageConfiguration {
974                accepted_output_modes: Some(vec!["text/plain".to_string()]),
975                task_push_notification_config: None,
976                history_length: Some(10),
977                return_immediately: Some(true),
978            }),
979            metadata: None,
980            tenant: Some("tenant-1".to_string()),
981        };
982        let json = serde_json::to_string(&req).unwrap();
983        let back: SendMessageRequest = serde_json::from_str(&json).unwrap();
984        assert_eq!(req.tenant, back.tenant);
985        assert_eq!(
986            req.configuration.as_ref().unwrap().history_length,
987            back.configuration.as_ref().unwrap().history_length
988        );
989    }
990
991    #[test]
992    fn test_role_all_variants_serde() {
993        let cases = [
994            (Role::Unspecified, "\"ROLE_UNSPECIFIED\""),
995            (Role::User, "\"ROLE_USER\""),
996            (Role::Agent, "\"ROLE_AGENT\""),
997        ];
998
999        for (role, expected_json) in cases {
1000            let json = serde_json::to_string(&role).unwrap();
1001            assert_eq!(json, expected_json);
1002            let back: Role = serde_json::from_str(&json).unwrap();
1003            assert_eq!(back, role);
1004        }
1005
1006        let back: Role = serde_json::from_str("\"\"").unwrap();
1007        assert_eq!(back, Role::Unspecified);
1008    }
1009
1010    #[test]
1011    fn test_task_state_all_variants_serde() {
1012        let cases = [
1013            (TaskState::Unspecified, "TASK_STATE_UNSPECIFIED"),
1014            (TaskState::Submitted, "TASK_STATE_SUBMITTED"),
1015            (TaskState::Working, "TASK_STATE_WORKING"),
1016            (TaskState::Completed, "TASK_STATE_COMPLETED"),
1017            (TaskState::Failed, "TASK_STATE_FAILED"),
1018            (TaskState::Canceled, "TASK_STATE_CANCELED"),
1019            (TaskState::InputRequired, "TASK_STATE_INPUT_REQUIRED"),
1020            (TaskState::Rejected, "TASK_STATE_REJECTED"),
1021            (TaskState::AuthRequired, "TASK_STATE_AUTH_REQUIRED"),
1022        ];
1023
1024        for (state, expected_str) in cases {
1025            let json = serde_json::to_string(&state).unwrap();
1026            assert_eq!(json, format!("\"{}\"", expected_str));
1027            let back: TaskState = serde_json::from_str(&json).unwrap();
1028            assert_eq!(back, state);
1029        }
1030
1031        let back: TaskState = serde_json::from_str("\"\"").unwrap();
1032        assert_eq!(back, TaskState::Unspecified);
1033    }
1034
1035    #[test]
1036    fn test_part_all_content_variants_serde() {
1037        let parts = [
1038            Part::text("hello"),
1039            Part::raw(vec![1, 2, 3]),
1040            Part::url("https://example.com"),
1041            Part::data(serde_json::json!({"value": 1})),
1042        ];
1043
1044        for part in parts {
1045            let json = serde_json::to_string(&part).unwrap();
1046            let back: Part = serde_json::from_str(&json).unwrap();
1047            match (&part.content, &back.content) {
1048                (PartContent::Text(a), PartContent::Text(b)) => assert_eq!(a, b),
1049                (PartContent::Raw(a), PartContent::Raw(b)) => assert_eq!(a, b),
1050                (PartContent::Url(a), PartContent::Url(b)) => assert_eq!(a, b),
1051                (PartContent::Data(_), PartContent::Data(_)) => {}
1052                _ => panic!("mismatched part content variants"),
1053            }
1054        }
1055    }
1056
1057    #[test]
1058    fn test_send_message_response_message_deserialize() {
1059        let json = serde_json::json!({
1060            "message": {
1061                "messageId": "m1",
1062                "role": "ROLE_AGENT",
1063                "parts": [{"text": "hello"}]
1064            }
1065        });
1066        let back: SendMessageResponse = serde_json::from_value(json).unwrap();
1067        assert!(matches!(back, SendMessageResponse::Message(_)));
1068    }
1069
1070    #[test]
1071    fn test_new_id_functions() {
1072        let task_id = new_task_id();
1073        assert!(!task_id.is_empty());
1074        let ctx_id = new_context_id();
1075        assert!(!ctx_id.is_empty());
1076        let msg_id = new_message_id();
1077        assert!(!msg_id.is_empty());
1078        let art_id = new_artifact_id();
1079        assert!(!art_id.is_empty());
1080        // All should be unique
1081        assert_ne!(task_id, ctx_id);
1082    }
1083
1084    #[test]
1085    fn test_role_default() {
1086        assert_eq!(Role::default(), Role::Unspecified);
1087    }
1088
1089    #[test]
1090    fn test_task_state_default() {
1091        assert_eq!(TaskState::default(), TaskState::Unspecified);
1092    }
1093
1094    #[test]
1095    fn test_list_tasks_request_serde() {
1096        let req = ListTasksRequest {
1097            context_id: Some("c1".to_string()),
1098            status: Some(TaskState::Working),
1099            page_size: Some(10),
1100            page_token: None,
1101            history_length: Some(5),
1102            status_timestamp_after: None,
1103            include_artifacts: Some(true),
1104            tenant: None,
1105        };
1106        let json = serde_json::to_string(&req).unwrap();
1107        let back: ListTasksRequest = serde_json::from_str(&json).unwrap();
1108        assert_eq!(req, back);
1109    }
1110
1111    #[test]
1112    fn test_cancel_task_request_serde() {
1113        let req = CancelTaskRequest {
1114            id: "t1".to_string(),
1115            metadata: None,
1116            tenant: Some("ten".to_string()),
1117        };
1118        let json = serde_json::to_string(&req).unwrap();
1119        let back: CancelTaskRequest = serde_json::from_str(&json).unwrap();
1120        assert_eq!(req, back);
1121    }
1122
1123    #[test]
1124    fn test_subscribe_to_task_request_serde() {
1125        let req = SubscribeToTaskRequest {
1126            id: "t1".to_string(),
1127            tenant: None,
1128        };
1129        let json = serde_json::to_string(&req).unwrap();
1130        let back: SubscribeToTaskRequest = serde_json::from_str(&json).unwrap();
1131        assert_eq!(req, back);
1132    }
1133}