Skip to main content

a2a_rs_core/
lib.rs

1//! Core data structures for A2A RC 1.0 JSON-RPC over HTTP.
2//! Provides shared types for server/client plus minimal helpers for JSON-RPC envelopes and error codes.
3//! Aligned with the authoritative proto definition (specification/a2a.proto).
4
5use serde::{Deserialize, Serialize};
6pub mod compat;
7
8use std::collections::HashMap;
9
10/// Deserialize a field that may be null, missing, or an array as Vec<T>.
11/// Handles the common case where servers send `"field": null` instead of omitting it.
12fn nullable_vec<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
13where
14    D: serde::Deserializer<'de>,
15    T: Deserialize<'de>,
16{
17    Option::<Vec<T>>::deserialize(deserializer).map(|v| v.unwrap_or_default())
18}
19
20/// Deserialize a field that may be null, missing, or a map as HashMap<K, V>.
21fn nullable_map<'de, D, K, V>(deserializer: D) -> Result<HashMap<K, V>, D::Error>
22where
23    D: serde::Deserializer<'de>,
24    K: Deserialize<'de> + std::cmp::Eq + std::hash::Hash,
25    V: Deserialize<'de>,
26{
27    Option::<HashMap<K, V>>::deserialize(deserializer).map(|v| v.unwrap_or_default())
28}
29use uuid::Uuid;
30
31pub const PROTOCOL_VERSION: &str = "1.0";
32
33// ---------- Agent Card ----------
34
35/// Complete Agent Card per A2A RC 1.0 proto spec
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
37#[serde(rename_all = "camelCase")]
38pub struct AgentCard {
39    /// Agent display name (primary identifier per proto)
40    pub name: String,
41    /// Agent description (required in RC 1.0)
42    pub description: String,
43    /// Supported transport interfaces (contains endpoint URLs)
44    #[serde(default, deserialize_with = "nullable_vec")]
45    pub supported_interfaces: Vec<AgentInterface>,
46    /// Provider/organization information
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub provider: Option<AgentProvider>,
49    /// Supported A2A protocol version (e.g., "1.0")
50    pub version: String,
51    /// Link to documentation
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub documentation_url: Option<String>,
54    /// Feature flags
55    #[serde(default)]
56    pub capabilities: AgentCapabilities,
57    /// Named authentication schemes (map from scheme name to scheme)
58    #[serde(default, deserialize_with = "nullable_map")]
59    pub security_schemes: HashMap<String, SecurityScheme>,
60    /// Required auth per operation
61    #[serde(default, deserialize_with = "nullable_vec")]
62    pub security_requirements: Vec<SecurityRequirement>,
63    /// Default accepted input MIME types
64    #[serde(default, deserialize_with = "nullable_vec")]
65    pub default_input_modes: Vec<String>,
66    /// Default output MIME types
67    #[serde(default, deserialize_with = "nullable_vec")]
68    pub default_output_modes: Vec<String>,
69    /// Agent capabilities/functions
70    #[serde(default, deserialize_with = "nullable_vec")]
71    pub skills: Vec<AgentSkill>,
72    /// Cryptographic signatures for verification
73    #[serde(default, skip_serializing_if = "Vec::is_empty", deserialize_with = "nullable_vec")]
74    pub signatures: Vec<AgentCardSignature>,
75    /// Icon URL for the agent
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub icon_url: Option<String>,
78}
79
80impl AgentCard {
81    /// Get the primary JSON-RPC endpoint URL from supported interfaces
82    pub fn endpoint(&self) -> Option<&str> {
83        self.supported_interfaces
84            .iter()
85            .find(|i| i.protocol_binding.eq_ignore_ascii_case("jsonrpc"))
86            .map(|i| i.url.as_str())
87    }
88}
89
90/// Agent interface / transport endpoint
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
92#[serde(rename_all = "camelCase")]
93pub struct AgentInterface {
94    pub url: String,
95    pub protocol_binding: String,
96    pub protocol_version: String,
97    #[serde(default, skip_serializing_if = "Option::is_none")]
98    pub tenant: Option<String>,
99}
100
101/// Provider/organization information
102#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
103#[serde(rename_all = "camelCase")]
104pub struct AgentProvider {
105    /// Organization name
106    pub organization: String,
107    /// Provider URL (required in RC 1.0)
108    pub url: String,
109}
110
111/// Authentication scheme definition (externally tagged, proto oneof)
112#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
113#[serde(rename_all = "camelCase")]
114#[non_exhaustive]
115pub enum SecurityScheme {
116    ApiKeySecurityScheme(ApiKeySecurityScheme),
117    HttpAuthSecurityScheme(HttpAuthSecurityScheme),
118    Oauth2SecurityScheme(OAuth2SecurityScheme),
119    OpenIdConnectSecurityScheme(OpenIdConnectSecurityScheme),
120    MtlsSecurityScheme(MutualTlsSecurityScheme),
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
124#[serde(rename_all = "camelCase")]
125pub struct ApiKeySecurityScheme {
126    #[serde(default, skip_serializing_if = "Option::is_none")]
127    pub description: Option<String>,
128    /// Location of the API key (e.g., "header", "query", "cookie")
129    pub location: String,
130    /// Name of the API key parameter
131    pub name: String,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
135#[serde(rename_all = "camelCase")]
136pub struct HttpAuthSecurityScheme {
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub description: Option<String>,
139    /// HTTP auth scheme (e.g., "bearer", "basic")
140    pub scheme: String,
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub bearer_format: Option<String>,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
146#[serde(rename_all = "camelCase")]
147pub struct OAuth2SecurityScheme {
148    #[serde(default, skip_serializing_if = "Option::is_none")]
149    pub description: Option<String>,
150    pub flows: OAuthFlows,
151    #[serde(default, skip_serializing_if = "Option::is_none")]
152    pub oauth2_metadata_url: Option<String>,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
156#[serde(rename_all = "camelCase")]
157pub struct OpenIdConnectSecurityScheme {
158    #[serde(default, skip_serializing_if = "Option::is_none")]
159    pub description: Option<String>,
160    pub open_id_connect_url: String,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
164#[serde(rename_all = "camelCase")]
165pub struct MutualTlsSecurityScheme {
166    #[serde(default, skip_serializing_if = "Option::is_none")]
167    pub description: Option<String>,
168}
169
170/// OAuth2 flows — proto oneof
171#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
172#[serde(rename_all = "camelCase")]
173#[non_exhaustive]
174pub enum OAuthFlows {
175    AuthorizationCode(AuthorizationCodeOAuthFlow),
176    ClientCredentials(ClientCredentialsOAuthFlow),
177    /// Deprecated per spec
178    Implicit(ImplicitOAuthFlow),
179    /// Deprecated per spec
180    Password(PasswordOAuthFlow),
181    DeviceCode(DeviceCodeOAuthFlow),
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
185#[serde(rename_all = "camelCase")]
186pub struct AuthorizationCodeOAuthFlow {
187    pub authorization_url: String,
188    pub token_url: String,
189    #[serde(default, skip_serializing_if = "Option::is_none")]
190    pub refresh_url: Option<String>,
191    #[serde(default)]
192    pub scopes: HashMap<String, String>,
193    #[serde(default, skip_serializing_if = "Option::is_none")]
194    pub pkce_required: Option<bool>,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
198#[serde(rename_all = "camelCase")]
199pub struct ClientCredentialsOAuthFlow {
200    pub token_url: String,
201    #[serde(default, skip_serializing_if = "Option::is_none")]
202    pub refresh_url: Option<String>,
203    #[serde(default)]
204    pub scopes: HashMap<String, String>,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
208#[serde(rename_all = "camelCase")]
209pub struct ImplicitOAuthFlow {
210    pub authorization_url: String,
211    #[serde(default, skip_serializing_if = "Option::is_none")]
212    pub refresh_url: Option<String>,
213    #[serde(default)]
214    pub scopes: HashMap<String, String>,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
218#[serde(rename_all = "camelCase")]
219pub struct PasswordOAuthFlow {
220    pub token_url: String,
221    #[serde(default, skip_serializing_if = "Option::is_none")]
222    pub refresh_url: Option<String>,
223    #[serde(default)]
224    pub scopes: HashMap<String, String>,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
228#[serde(rename_all = "camelCase")]
229pub struct DeviceCodeOAuthFlow {
230    pub device_authorization_url: String,
231    pub token_url: String,
232    #[serde(default, skip_serializing_if = "Option::is_none")]
233    pub refresh_url: Option<String>,
234    #[serde(default)]
235    pub scopes: HashMap<String, String>,
236}
237
238/// Helper struct for lists of strings in security requirements
239#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
240#[serde(rename_all = "camelCase")]
241pub struct StringList {
242    #[serde(default)]
243    pub list: Vec<String>,
244}
245
246/// Security requirement for operations
247#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
248#[serde(rename_all = "camelCase")]
249pub struct SecurityRequirement {
250    /// Map from scheme name to required scopes
251    #[serde(default)]
252    pub schemes: HashMap<String, StringList>,
253}
254
255/// Cryptographic signature for Agent Card verification (JWS)
256#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
257#[serde(rename_all = "camelCase")]
258pub struct AgentCardSignature {
259    /// JWS protected header (base64url-encoded)
260    pub protected: String,
261    /// JWS signature
262    pub signature: String,
263    /// JWS unprotected header
264    #[serde(default, skip_serializing_if = "Option::is_none")]
265    pub header: Option<serde_json::Value>,
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
269#[serde(rename_all = "camelCase")]
270pub struct AgentCapabilities {
271    #[serde(default, skip_serializing_if = "Option::is_none")]
272    pub streaming: Option<bool>,
273    #[serde(default, skip_serializing_if = "Option::is_none")]
274    pub push_notifications: Option<bool>,
275    #[serde(default, skip_serializing_if = "Option::is_none")]
276    pub extended_agent_card: Option<bool>,
277    #[serde(default, skip_serializing_if = "Vec::is_empty", deserialize_with = "nullable_vec")]
278    pub extensions: Vec<AgentExtension>,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
282#[serde(rename_all = "camelCase")]
283pub struct AgentExtension {
284    pub uri: String,
285    #[serde(default, skip_serializing_if = "Option::is_none")]
286    pub description: Option<String>,
287    #[serde(default, skip_serializing_if = "Option::is_none")]
288    pub required: Option<bool>,
289    #[serde(default, skip_serializing_if = "Option::is_none")]
290    pub params: Option<serde_json::Value>,
291}
292
293/// Agent skill/capability definition
294#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
295#[serde(rename_all = "camelCase")]
296pub struct AgentSkill {
297    /// Unique skill identifier
298    pub id: String,
299    /// Display name
300    pub name: String,
301    /// Capability description
302    pub description: String,
303    /// Classification tags
304    #[serde(default, deserialize_with = "nullable_vec")]
305    pub tags: Vec<String>,
306    /// Example prompts or inputs
307    #[serde(default, deserialize_with = "nullable_vec")]
308    pub examples: Vec<String>,
309    /// Accepted input MIME types
310    #[serde(default, deserialize_with = "nullable_vec")]
311    pub input_modes: Vec<String>,
312    /// Produced output MIME types
313    #[serde(default, deserialize_with = "nullable_vec")]
314    pub output_modes: Vec<String>,
315    /// Security requirements for this skill
316    #[serde(default, deserialize_with = "nullable_vec")]
317    pub security_requirements: Vec<SecurityRequirement>,
318}
319
320// ---------- Content Parts ----------
321
322/// Part content — flat proto-JSON format per A2A v1.0 spec.
323///
324/// Uses custom serialization to produce flat JSON without a `kind` discriminator:
325/// - `Part::Text` → `{"text": "..."}`
326/// - `Part::File` → `{"file": {"uri": "...", "mimeType": "..."}}`
327/// - `Part::Data` → `{"data": {...}}`
328///
329/// Deserialization detects the variant by which field is present.
330#[derive(Debug, Clone, PartialEq)]
331pub enum Part {
332    /// Text content part
333    Text {
334        /// The text content
335        text: String,
336        /// Part-level metadata
337        metadata: Option<serde_json::Value>,
338    },
339    /// File content part (inline bytes or URI reference)
340    File {
341        /// File content
342        file: FileContent,
343        /// Part-level metadata
344        metadata: Option<serde_json::Value>,
345    },
346    /// Structured data part
347    Data {
348        /// Structured JSON data
349        data: serde_json::Value,
350        /// Part-level metadata
351        metadata: Option<serde_json::Value>,
352    },
353}
354
355impl Serialize for Part {
356    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
357        use serde::ser::SerializeMap;
358        match self {
359            Part::Text { text, metadata } => {
360                let mut map = serializer.serialize_map(None)?;
361                map.serialize_entry("kind", "text")?;
362                map.serialize_entry("text", text)?;
363                if let Some(m) = metadata {
364                    map.serialize_entry("metadata", m)?;
365                }
366                map.end()
367            }
368            Part::File { file, metadata } => {
369                let mut map = serializer.serialize_map(None)?;
370                map.serialize_entry("kind", "file")?;
371                map.serialize_entry("file", file)?;
372                if let Some(m) = metadata {
373                    map.serialize_entry("metadata", m)?;
374                }
375                map.end()
376            }
377            Part::Data { data, metadata } => {
378                let mut map = serializer.serialize_map(None)?;
379                map.serialize_entry("kind", "data")?;
380                map.serialize_entry("data", data)?;
381                if let Some(m) = metadata {
382                    map.serialize_entry("metadata", m)?;
383                }
384                map.end()
385            }
386        }
387    }
388}
389
390impl<'de> Deserialize<'de> for Part {
391    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
392        let value = serde_json::Value::deserialize(deserializer)?;
393        let obj = value
394            .as_object()
395            .ok_or_else(|| serde::de::Error::custom("expected object for Part"))?;
396        let metadata = obj.get("metadata").cloned();
397
398        if let Some(text) = obj.get("text") {
399            Ok(Part::Text {
400                text: text
401                    .as_str()
402                    .ok_or_else(|| serde::de::Error::custom("text must be a string"))?
403                    .to_string(),
404                metadata,
405            })
406        } else if let Some(file) = obj.get("file") {
407            let file: FileContent = serde_json::from_value(file.clone())
408                .map_err(serde::de::Error::custom)?;
409            Ok(Part::File { file, metadata })
410        } else if let Some(data) = obj.get("data") {
411            Ok(Part::Data {
412                data: data.clone(),
413                metadata,
414            })
415        } else if obj.contains_key("raw") || obj.contains_key("url") {
416            // Proto-style: raw/url as top-level fields (not nested under file)
417            let file = FileContent {
418                bytes: obj.get("raw").and_then(|v| v.as_str()).map(|s| s.to_string()),
419                uri: obj.get("url").and_then(|v| v.as_str()).map(|s| s.to_string()),
420                name: obj.get("filename").and_then(|v| v.as_str()).map(|s| s.to_string()),
421                mime_type: obj
422                    .get("mediaType")
423                    .or_else(|| obj.get("mimeType"))
424                    .and_then(|v| v.as_str())
425                    .map(|s| s.to_string()),
426            };
427            Ok(Part::File { file, metadata })
428        } else if obj.contains_key("kind") {
429            // v0.3 compatibility: handle kind-discriminated parts
430            let kind = obj["kind"].as_str().unwrap_or("");
431            match kind {
432                "text" => Ok(Part::Text {
433                    text: obj
434                        .get("text")
435                        .and_then(|v| v.as_str())
436                        .unwrap_or("")
437                        .to_string(),
438                    metadata,
439                }),
440                "file" => {
441                    let file: FileContent = serde_json::from_value(
442                        obj.get("file").cloned().unwrap_or_default(),
443                    )
444                    .map_err(serde::de::Error::custom)?;
445                    Ok(Part::File { file, metadata })
446                }
447                "data" => Ok(Part::Data {
448                    data: obj.get("data").cloned().unwrap_or_default(),
449                    metadata,
450                }),
451                _ => Err(serde::de::Error::custom(format!(
452                    "unknown part kind: {kind}"
453                ))),
454            }
455        } else {
456            Err(serde::de::Error::custom(
457                "Part must have text, file, or data field",
458            ))
459        }
460    }
461}
462
463/// File content — either inline bytes or a URI reference.
464#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
465#[serde(rename_all = "camelCase")]
466pub struct FileContent {
467    /// Base64-encoded file bytes
468    #[serde(default, skip_serializing_if = "Option::is_none")]
469    pub bytes: Option<String>,
470    /// URI pointing to the file
471    #[serde(default, skip_serializing_if = "Option::is_none")]
472    pub uri: Option<String>,
473    /// File name
474    #[serde(default, skip_serializing_if = "Option::is_none")]
475    pub name: Option<String>,
476    /// MIME type
477    #[serde(default, skip_serializing_if = "Option::is_none")]
478    pub mime_type: Option<String>,
479}
480
481impl Part {
482    /// Create a text part
483    pub fn text(text: impl Into<String>) -> Self {
484        Part::Text {
485            text: text.into(),
486            metadata: None,
487        }
488    }
489
490    /// Create a file part with a URI reference
491    pub fn file_uri(uri: impl Into<String>, mime_type: impl Into<String>) -> Self {
492        Part::File {
493            file: FileContent {
494                bytes: None,
495                uri: Some(uri.into()),
496                name: None,
497                mime_type: Some(mime_type.into()),
498            },
499            metadata: None,
500        }
501    }
502
503    /// Create a file part with inline bytes (base64-encoded)
504    pub fn file_bytes(bytes: impl Into<String>, mime_type: impl Into<String>) -> Self {
505        Part::File {
506            file: FileContent {
507                bytes: Some(bytes.into()),
508                uri: None,
509                name: None,
510                mime_type: Some(mime_type.into()),
511            },
512            metadata: None,
513        }
514    }
515
516    /// Create a structured data part
517    pub fn data(data: serde_json::Value) -> Self {
518        Part::Data {
519            data,
520            metadata: None,
521        }
522    }
523
524    /// Get the text content, if this is a text part
525    pub fn as_text(&self) -> Option<&str> {
526        match self {
527            Part::Text { text, .. } => Some(text),
528            _ => None,
529        }
530    }
531}
532
533// ---------- Messages, Tasks, Artifacts ----------
534
535/// Message role per proto spec
536#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
537#[non_exhaustive]
538pub enum Role {
539    #[serde(rename = "ROLE_UNSPECIFIED")]
540    Unspecified,
541    #[serde(rename = "ROLE_USER")]
542    User,
543    #[serde(rename = "ROLE_AGENT")]
544    Agent,
545}
546
547/// Message structure per A2A RC 1.0 spec
548#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
549#[serde(rename_all = "camelCase")]
550pub struct Message {
551    /// Kind discriminator — always "message"
552    #[serde(default = "default_message_kind")]
553    pub kind: String,
554    /// Unique message identifier
555    pub message_id: String,
556    /// Optional conversation context ID
557    #[serde(default, skip_serializing_if = "Option::is_none")]
558    pub context_id: Option<String>,
559    /// Optional task reference
560    #[serde(default, skip_serializing_if = "Option::is_none")]
561    pub task_id: Option<String>,
562    /// Message role (user or agent)
563    pub role: Role,
564    /// Message content parts
565    pub parts: Vec<Part>,
566    /// Custom metadata
567    #[serde(default, skip_serializing_if = "Option::is_none")]
568    pub metadata: Option<serde_json::Value>,
569    /// Extension URIs
570    #[serde(default, skip_serializing_if = "Vec::is_empty", deserialize_with = "nullable_vec")]
571    pub extensions: Vec<String>,
572    /// Optional related task IDs
573    #[serde(default, skip_serializing_if = "Option::is_none")]
574    pub reference_task_ids: Option<Vec<String>>,
575}
576
577fn default_message_kind() -> String {
578    "message".to_string()
579}
580
581fn default_task_kind() -> String {
582    "task".to_string()
583}
584
585fn default_status_update_kind() -> String {
586    "status-update".to_string()
587}
588
589fn default_artifact_update_kind() -> String {
590    "artifact-update".to_string()
591}
592
593/// Artifact output from task processing per proto spec
594#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
595#[serde(rename_all = "camelCase")]
596pub struct Artifact {
597    /// Unique artifact identifier
598    pub artifact_id: String,
599    /// Optional display name
600    #[serde(default, skip_serializing_if = "Option::is_none")]
601    pub name: Option<String>,
602    /// Optional description
603    #[serde(default, skip_serializing_if = "Option::is_none")]
604    pub description: Option<String>,
605    /// Artifact content parts
606    pub parts: Vec<Part>,
607    /// Custom metadata
608    #[serde(default, skip_serializing_if = "Option::is_none")]
609    pub metadata: Option<serde_json::Value>,
610    /// Extension URIs
611    #[serde(default, skip_serializing_if = "Vec::is_empty", deserialize_with = "nullable_vec")]
612    pub extensions: Vec<String>,
613}
614
615/// Task lifecycle state per proto spec
616#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
617#[non_exhaustive]
618pub enum TaskState {
619    #[serde(rename = "TASK_STATE_UNSPECIFIED")]
620    Unspecified,
621    #[serde(rename = "TASK_STATE_SUBMITTED")]
622    Submitted,
623    #[serde(rename = "TASK_STATE_WORKING")]
624    Working,
625    #[serde(rename = "TASK_STATE_COMPLETED")]
626    Completed,
627    #[serde(rename = "TASK_STATE_FAILED")]
628    Failed,
629    #[serde(rename = "TASK_STATE_CANCELED")]
630    Canceled,
631    #[serde(rename = "TASK_STATE_INPUT_REQUIRED")]
632    InputRequired,
633    #[serde(rename = "TASK_STATE_REJECTED")]
634    Rejected,
635    #[serde(rename = "TASK_STATE_AUTH_REQUIRED")]
636    AuthRequired,
637}
638
639/// Task status information
640#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
641#[serde(rename_all = "camelCase")]
642pub struct TaskStatus {
643    /// Current lifecycle state
644    pub state: TaskState,
645    /// Optional status message
646    #[serde(default, skip_serializing_if = "Option::is_none")]
647    pub message: Option<Message>,
648    /// ISO 8601 timestamp (e.g., "2023-10-27T10:00:00Z")
649    #[serde(default, skip_serializing_if = "Option::is_none")]
650    pub timestamp: Option<String>,
651}
652
653/// Task resource per A2A RC 1.0 spec
654#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
655#[serde(rename_all = "camelCase")]
656pub struct Task {
657    /// Kind discriminator — always "task"
658    #[serde(default = "default_task_kind")]
659    pub kind: String,
660    /// Unique task identifier (UUID)
661    pub id: String,
662    /// Context identifier for grouping related interactions
663    pub context_id: String,
664    /// Current task status
665    pub status: TaskStatus,
666    /// Optional output artifacts
667    #[serde(default, skip_serializing_if = "Option::is_none")]
668    pub artifacts: Option<Vec<Artifact>>,
669    /// Optional message history
670    #[serde(default, skip_serializing_if = "Option::is_none")]
671    pub history: Option<Vec<Message>>,
672    /// Custom metadata
673    #[serde(default, skip_serializing_if = "Option::is_none")]
674    pub metadata: Option<serde_json::Value>,
675}
676
677impl TaskState {
678    /// Check if state is terminal (no further updates expected)
679    pub fn is_terminal(&self) -> bool {
680        matches!(
681            self,
682            TaskState::Completed | TaskState::Failed | TaskState::Canceled | TaskState::Rejected
683        )
684    }
685}
686
687// ---------- SendMessage response ----------
688
689/// Handler-level response from SendMessage — can be a Task or direct Message.
690///
691/// Uses externally tagged serialization for internal pattern matching.
692/// For the wire format (JSON-RPC result field), use [`SendMessageResult`].
693#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
694#[serde(rename_all = "camelCase")]
695pub enum SendMessageResponse {
696    Task(Task),
697    Message(Message),
698}
699
700/// Wire-format result for message/send — the value inside the JSON-RPC `result` field.
701///
702/// Uses externally tagged serialization per v1.0 proto-JSON:
703/// - `{"task": {...}}` or `{"message": {...}}`
704#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
705#[serde(rename_all = "camelCase")]
706pub enum SendMessageResult {
707    Task(Task),
708    Message(Message),
709}
710
711// ---------- JSON-RPC helper types ----------
712
713#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
714pub struct JsonRpcRequest {
715    pub jsonrpc: String,
716    pub method: String,
717    #[serde(default)]
718    pub params: Option<serde_json::Value>,
719    pub id: serde_json::Value,
720}
721
722#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
723pub struct JsonRpcResponse {
724    pub jsonrpc: String,
725    pub id: serde_json::Value,
726    #[serde(skip_serializing_if = "Option::is_none")]
727    pub result: Option<serde_json::Value>,
728    #[serde(skip_serializing_if = "Option::is_none")]
729    pub error: Option<JsonRpcError>,
730}
731
732#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
733pub struct JsonRpcError {
734    pub code: i32,
735    pub message: String,
736    #[serde(skip_serializing_if = "Option::is_none")]
737    pub data: Option<serde_json::Value>,
738}
739
740/// JSON-RPC and A2A-specific error codes
741pub mod errors {
742    // Standard JSON-RPC 2.0 errors
743    pub const PARSE_ERROR: i32 = -32700;
744    pub const INVALID_REQUEST: i32 = -32600;
745    pub const METHOD_NOT_FOUND: i32 = -32601;
746    pub const INVALID_PARAMS: i32 = -32602;
747    pub const INTERNAL_ERROR: i32 = -32603;
748
749    // A2A-specific errors
750    pub const TASK_NOT_FOUND: i32 = -32001;
751    pub const TASK_NOT_CANCELABLE: i32 = -32002;
752    pub const PUSH_NOTIFICATION_NOT_SUPPORTED: i32 = -32003;
753    pub const UNSUPPORTED_OPERATION: i32 = -32004;
754    pub const CONTENT_TYPE_NOT_SUPPORTED: i32 = -32005;
755    pub const EXTENDED_AGENT_CARD_NOT_CONFIGURED: i32 = -32006;
756    pub const VERSION_NOT_SUPPORTED: i32 = -32007;
757    pub const INVALID_AGENT_RESPONSE: i32 = -32008;
758    pub const EXTENSION_SUPPORT_REQUIRED: i32 = -32009;
759
760    pub fn message_for_code(code: i32) -> &'static str {
761        match code {
762            PARSE_ERROR => "Parse error",
763            INVALID_REQUEST => "Invalid request",
764            METHOD_NOT_FOUND => "Method not found",
765            INVALID_PARAMS => "Invalid params",
766            INTERNAL_ERROR => "Internal error",
767            TASK_NOT_FOUND => "Task not found",
768            TASK_NOT_CANCELABLE => "Task not cancelable",
769            PUSH_NOTIFICATION_NOT_SUPPORTED => "Push notifications not supported",
770            UNSUPPORTED_OPERATION => "Unsupported operation",
771            CONTENT_TYPE_NOT_SUPPORTED => "Content type not supported",
772            EXTENDED_AGENT_CARD_NOT_CONFIGURED => "Extended agent card not configured",
773            VERSION_NOT_SUPPORTED => "Protocol version not supported",
774            INVALID_AGENT_RESPONSE => "Invalid agent response",
775            EXTENSION_SUPPORT_REQUIRED => "Extension support required",
776            _ => "Unknown error",
777        }
778    }
779}
780
781pub fn success(id: serde_json::Value, result: serde_json::Value) -> JsonRpcResponse {
782    JsonRpcResponse {
783        jsonrpc: "2.0".to_string(),
784        id,
785        result: Some(result),
786        error: None,
787    }
788}
789
790pub fn error(
791    id: serde_json::Value,
792    code: i32,
793    message: &str,
794    data: Option<serde_json::Value>,
795) -> JsonRpcResponse {
796    JsonRpcResponse {
797        jsonrpc: "2.0".to_string(),
798        id,
799        result: None,
800        error: Some(JsonRpcError {
801            code,
802            message: message.to_string(),
803            data,
804        }),
805    }
806}
807
808// ---------- Method params ----------
809
810/// Parameters for message/send operation
811#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
812#[serde(rename_all = "camelCase")]
813pub struct SendMessageRequest {
814    /// Optional tenant for multi-tenancy
815    #[serde(default, skip_serializing_if = "Option::is_none")]
816    pub tenant: Option<String>,
817    /// The message to send
818    pub message: Message,
819    /// Optional request configuration
820    #[serde(default, skip_serializing_if = "Option::is_none")]
821    pub configuration: Option<SendMessageConfiguration>,
822    /// Custom metadata
823    #[serde(default, skip_serializing_if = "Option::is_none")]
824    pub metadata: Option<serde_json::Value>,
825}
826
827/// Configuration for message send requests
828#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
829#[serde(rename_all = "camelCase")]
830pub struct SendMessageConfiguration {
831    /// Preferred output MIME types
832    #[serde(default, skip_serializing_if = "Option::is_none")]
833    pub accepted_output_modes: Option<Vec<String>>,
834    /// Push notification configuration for this request
835    #[serde(default, skip_serializing_if = "Option::is_none")]
836    pub push_notification_config: Option<PushNotificationConfig>,
837    /// Message history depth (0 = omit, None = server default)
838    #[serde(default, skip_serializing_if = "Option::is_none")]
839    pub history_length: Option<u32>,
840    /// Wait for task completion (default: false)
841    #[serde(default, skip_serializing_if = "Option::is_none")]
842    pub blocking: Option<bool>,
843    /// Return immediately without waiting for completion (default: false).
844    /// When true, the server returns the task in its current state even if non-terminal.
845    #[serde(default, skip_serializing_if = "Option::is_none")]
846    pub return_immediately: Option<bool>,
847}
848
849/// Parameters for tasks/get operation
850#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
851#[serde(rename_all = "camelCase")]
852pub struct GetTaskRequest {
853    /// Task ID
854    pub id: String,
855    /// Message history depth
856    #[serde(default, skip_serializing_if = "Option::is_none")]
857    pub history_length: Option<u32>,
858    /// Optional tenant
859    #[serde(default, skip_serializing_if = "Option::is_none")]
860    pub tenant: Option<String>,
861}
862
863/// Parameters for tasks/cancel operation
864#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
865#[serde(rename_all = "camelCase")]
866pub struct CancelTaskRequest {
867    /// Task ID
868    pub id: String,
869    /// Optional tenant
870    #[serde(default, skip_serializing_if = "Option::is_none")]
871    pub tenant: Option<String>,
872}
873
874/// Parameters for tasks/list operation
875#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
876#[serde(rename_all = "camelCase")]
877pub struct ListTasksRequest {
878    /// Filter by context ID
879    #[serde(default, skip_serializing_if = "Option::is_none")]
880    pub context_id: Option<String>,
881    /// Filter by task state
882    #[serde(default, skip_serializing_if = "Option::is_none")]
883    pub status: Option<TaskState>,
884    /// Results per page (1-100, default 50)
885    #[serde(default, skip_serializing_if = "Option::is_none")]
886    pub page_size: Option<u32>,
887    /// Pagination cursor
888    #[serde(default, skip_serializing_if = "Option::is_none")]
889    pub page_token: Option<String>,
890    /// History depth per task
891    #[serde(default, skip_serializing_if = "Option::is_none")]
892    pub history_length: Option<u32>,
893    /// Filter by status timestamp after (ISO 8601 or millis)
894    #[serde(default, skip_serializing_if = "Option::is_none")]
895    pub status_timestamp_after: Option<i64>,
896    /// Include artifacts in response
897    #[serde(default, skip_serializing_if = "Option::is_none")]
898    pub include_artifacts: Option<bool>,
899    /// Optional tenant
900    #[serde(default, skip_serializing_if = "Option::is_none")]
901    pub tenant: Option<String>,
902}
903
904/// Response for tasks/list operation
905#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
906#[serde(rename_all = "camelCase")]
907pub struct TaskListResponse {
908    /// Tasks matching the query
909    pub tasks: Vec<Task>,
910    /// Empty string if this is the final page
911    pub next_page_token: String,
912    /// Requested page size
913    pub page_size: u32,
914    /// Total matching tasks
915    pub total_size: u32,
916}
917
918/// Parameters for tasks/subscribe operation
919#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
920#[serde(rename_all = "camelCase")]
921pub struct SubscribeToTaskRequest {
922    /// Task ID
923    pub id: String,
924    /// Optional tenant
925    #[serde(default, skip_serializing_if = "Option::is_none")]
926    pub tenant: Option<String>,
927}
928
929/// Push notification configuration per proto spec
930#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
931#[serde(rename_all = "camelCase")]
932pub struct PushNotificationConfig {
933    /// Configuration identifier
934    #[serde(default, skip_serializing_if = "Option::is_none")]
935    pub id: Option<String>,
936    /// Webhook URL to receive notifications
937    pub url: String,
938    /// Token for webhook authentication
939    #[serde(default, skip_serializing_if = "Option::is_none")]
940    pub token: Option<String>,
941    /// Authentication details for webhook delivery
942    #[serde(default, skip_serializing_if = "Option::is_none")]
943    pub authentication: Option<AuthenticationInfo>,
944}
945
946/// Authentication info for push notification delivery per proto spec
947#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
948#[serde(rename_all = "camelCase")]
949pub struct AuthenticationInfo {
950    /// Auth scheme (e.g., "bearer", "api_key")
951    pub scheme: String,
952    /// Credentials (e.g., token value) — optional in RC 1.0
953    #[serde(default, skip_serializing_if = "Option::is_none")]
954    pub credentials: Option<String>,
955}
956
957/// Wrapper for push notification config with task context
958#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
959#[serde(rename_all = "camelCase")]
960pub struct TaskPushNotificationConfig {
961    /// Configuration ID
962    pub id: String,
963    /// Associated task ID
964    pub task_id: String,
965    /// The push notification configuration
966    pub push_notification_config: PushNotificationConfig,
967    /// Optional tenant
968    #[serde(default, skip_serializing_if = "Option::is_none")]
969    pub tenant: Option<String>,
970}
971
972/// Parameters for tasks/pushNotificationConfig/create
973#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
974#[serde(rename_all = "camelCase")]
975pub struct CreateTaskPushNotificationConfigRequest {
976    /// Task ID
977    pub task_id: String,
978    /// Configuration identifier
979    pub config_id: String,
980    /// Configuration details
981    pub push_notification_config: PushNotificationConfig,
982    /// Optional tenant
983    #[serde(default, skip_serializing_if = "Option::is_none")]
984    pub tenant: Option<String>,
985}
986
987/// Parameters for tasks/pushNotificationConfig/get
988#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
989#[serde(rename_all = "camelCase")]
990pub struct GetTaskPushNotificationConfigRequest {
991    /// Config ID
992    pub id: String,
993    /// Task ID
994    pub task_id: String,
995    /// Optional tenant
996    #[serde(default, skip_serializing_if = "Option::is_none")]
997    pub tenant: Option<String>,
998}
999
1000/// Parameters for tasks/pushNotificationConfig/list
1001#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1002#[serde(rename_all = "camelCase")]
1003pub struct ListTaskPushNotificationConfigRequest {
1004    /// Task ID
1005    pub task_id: String,
1006    /// Max configs to return
1007    #[serde(default, skip_serializing_if = "Option::is_none")]
1008    pub page_size: Option<u32>,
1009    /// Pagination cursor
1010    #[serde(default, skip_serializing_if = "Option::is_none")]
1011    pub page_token: Option<String>,
1012    /// Optional tenant
1013    #[serde(default, skip_serializing_if = "Option::is_none")]
1014    pub tenant: Option<String>,
1015}
1016
1017/// Response for tasks/pushNotificationConfig/list
1018#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1019#[serde(rename_all = "camelCase")]
1020pub struct ListTaskPushNotificationConfigResponse {
1021    /// Push notification configurations
1022    pub configs: Vec<TaskPushNotificationConfig>,
1023    /// Next page token
1024    #[serde(default)]
1025    pub next_page_token: String,
1026}
1027
1028/// Parameters for tasks/pushNotificationConfig/delete
1029#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1030#[serde(rename_all = "camelCase")]
1031pub struct DeleteTaskPushNotificationConfigRequest {
1032    /// Config ID
1033    pub id: String,
1034    /// Task ID
1035    pub task_id: String,
1036    /// Optional tenant
1037    #[serde(default, skip_serializing_if = "Option::is_none")]
1038    pub tenant: Option<String>,
1039}
1040
1041/// Parameters for agentCard/getExtended
1042#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1043#[serde(rename_all = "camelCase")]
1044pub struct GetExtendedAgentCardRequest {
1045    /// Optional tenant
1046    #[serde(default, skip_serializing_if = "Option::is_none")]
1047    pub tenant: Option<String>,
1048}
1049
1050// ---------- Streaming event types ----------
1051
1052/// Streaming response per proto spec — uses externally tagged oneof
1053#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1054#[serde(rename_all = "camelCase")]
1055pub enum StreamResponse {
1056    /// Complete task snapshot
1057    Task(Task),
1058    /// Direct message response
1059    Message(Message),
1060    /// Task status update event
1061    StatusUpdate(TaskStatusUpdateEvent),
1062    /// Task artifact update event
1063    ArtifactUpdate(TaskArtifactUpdateEvent),
1064}
1065
1066/// Task status update event
1067#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1068#[serde(rename_all = "camelCase")]
1069pub struct TaskStatusUpdateEvent {
1070    /// Kind discriminator — always "status-update"
1071    #[serde(default = "default_status_update_kind")]
1072    pub kind: String,
1073    /// Task identifier
1074    pub task_id: String,
1075    /// Context identifier
1076    pub context_id: String,
1077    /// Updated status
1078    pub status: TaskStatus,
1079    /// Whether this is the final event in the stream
1080    #[serde(rename = "final", default)]
1081    pub is_final: bool,
1082    /// Custom metadata
1083    #[serde(default, skip_serializing_if = "Option::is_none")]
1084    pub metadata: Option<serde_json::Value>,
1085}
1086
1087/// Task artifact update event
1088#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1089#[serde(rename_all = "camelCase")]
1090pub struct TaskArtifactUpdateEvent {
1091    /// Kind discriminator — always "artifact-update"
1092    #[serde(default = "default_artifact_update_kind")]
1093    pub kind: String,
1094    /// Task identifier
1095    pub task_id: String,
1096    /// Context identifier
1097    pub context_id: String,
1098    /// New or updated artifact
1099    pub artifact: Artifact,
1100    /// Whether to append to existing artifact
1101    #[serde(default, skip_serializing_if = "Option::is_none")]
1102    pub append: Option<bool>,
1103    /// Whether this is the last chunk
1104    #[serde(default, skip_serializing_if = "Option::is_none")]
1105    pub last_chunk: Option<bool>,
1106    /// Custom metadata
1107    #[serde(default, skip_serializing_if = "Option::is_none")]
1108    pub metadata: Option<serde_json::Value>,
1109}
1110
1111/// Wire-format result for streaming events — the value inside each SSE JSON-RPC `result` field.
1112///
1113/// Uses externally tagged serialization per v1.0 proto-JSON:
1114/// - `{"statusUpdate": {...}}`, `{"artifactUpdate": {...}}`, `{"task": {...}}`, `{"message": {...}}`
1115#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1116#[serde(rename_all = "camelCase")]
1117pub enum StreamingMessageResult {
1118    StatusUpdate(TaskStatusUpdateEvent),
1119    ArtifactUpdate(TaskArtifactUpdateEvent),
1120    Task(Task),
1121    Message(Message),
1122}
1123
1124// ---------- Helper functions ----------
1125
1126/// Create a new message with text content
1127pub fn new_message(role: Role, text: &str, context_id: Option<String>) -> Message {
1128    Message {
1129        kind: "message".to_string(),
1130        message_id: Uuid::new_v4().to_string(),
1131        context_id,
1132        task_id: None,
1133        role,
1134        parts: vec![Part::text(text)],
1135        metadata: None,
1136        extensions: vec![],
1137        reference_task_ids: None,
1138    }
1139}
1140
1141/// Create a completed task with text response
1142pub fn completed_task_with_text(user_message: Message, reply_text: &str) -> Task {
1143    let context_id = user_message
1144        .context_id
1145        .clone()
1146        .unwrap_or_else(|| Uuid::new_v4().to_string());
1147    let task_id = Uuid::new_v4().to_string();
1148    let agent_msg = new_message(Role::Agent, reply_text, Some(context_id.clone()));
1149
1150    Task {
1151        kind: "task".to_string(),
1152        id: task_id,
1153        context_id,
1154        status: TaskStatus {
1155            state: TaskState::Completed,
1156            message: Some(agent_msg.clone()),
1157            timestamp: Some(chrono::Utc::now().to_rfc3339()),
1158        },
1159        history: Some(vec![user_message, agent_msg]),
1160        artifacts: None,
1161        metadata: None,
1162    }
1163}
1164
1165/// Generate ISO 8601 timestamp
1166pub fn now_iso8601() -> String {
1167    chrono::Utc::now().to_rfc3339()
1168}
1169
1170/// Validate task ID format (UUID)
1171pub fn validate_task_id(id: &str) -> bool {
1172    Uuid::parse_str(id).is_ok()
1173}
1174
1175#[cfg(test)]
1176mod tests {
1177    use super::*;
1178
1179    #[test]
1180    fn jsonrpc_helpers_round_trip() {
1181        let resp = success(serde_json::json!(1), serde_json::json!({"ok": true}));
1182        assert_eq!(resp.jsonrpc, "2.0");
1183        assert!(resp.error.is_none());
1184        assert!(resp.result.is_some());
1185    }
1186
1187    #[test]
1188    fn task_state_is_terminal() {
1189        assert!(TaskState::Completed.is_terminal());
1190        assert!(TaskState::Failed.is_terminal());
1191        assert!(TaskState::Canceled.is_terminal());
1192        assert!(TaskState::Rejected.is_terminal());
1193        assert!(!TaskState::Working.is_terminal());
1194        assert!(!TaskState::Submitted.is_terminal());
1195        assert!(!TaskState::InputRequired.is_terminal());
1196    }
1197
1198    #[test]
1199    fn task_state_serialization() {
1200        let state = TaskState::Working;
1201        let json = serde_json::to_string(&state).unwrap();
1202        assert_eq!(json, r#""TASK_STATE_WORKING""#);
1203
1204        let parsed: TaskState = serde_json::from_str(&json).unwrap();
1205        assert_eq!(parsed, TaskState::Working);
1206    }
1207
1208    #[test]
1209    fn role_serialization() {
1210        let role = Role::User;
1211        let json = serde_json::to_string(&role).unwrap();
1212        assert_eq!(json, r#""ROLE_USER""#);
1213    }
1214
1215    #[test]
1216    fn message_serialization() {
1217        let msg = new_message(Role::User, "hello", Some("ctx-123".to_string()));
1218        let json = serde_json::to_string(&msg).unwrap();
1219        let parsed: Message = serde_json::from_str(&json).unwrap();
1220        assert_eq!(parsed.role, Role::User);
1221        assert_eq!(parsed.parts.len(), 1);
1222        assert_eq!(parsed.parts[0].as_text(), Some("hello"));
1223
1224        // Verify camelCase field names
1225        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1226        assert!(value.get("messageId").is_some());
1227        assert!(value.get("contextId").is_some());
1228    }
1229
1230    #[test]
1231    fn task_serialization() {
1232        let user_msg = new_message(Role::User, "test", None);
1233        let task = completed_task_with_text(user_msg, "response");
1234        let json = serde_json::to_string(&task).unwrap();
1235        let parsed: Task = serde_json::from_str(&json).unwrap();
1236        assert_eq!(parsed.status.state, TaskState::Completed);
1237        assert!(parsed.history.is_some());
1238        assert_eq!(parsed.history.unwrap().len(), 2);
1239
1240        // Verify camelCase
1241        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1242        assert!(value.get("contextId").is_some());
1243    }
1244
1245    #[test]
1246    fn part_text_serialization() {
1247        let part = Part::text("hello");
1248        let json = serde_json::to_string(&part).unwrap();
1249        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1250        assert_eq!(value.get("kind").unwrap().as_str().unwrap(), "text");
1251        assert_eq!(value.get("text").unwrap().as_str().unwrap(), "hello");
1252    }
1253
1254    #[test]
1255    fn part_text_round_trip() {
1256        let part = Part::text("hello");
1257        let json = serde_json::to_string(&part).unwrap();
1258        let parsed: Part = serde_json::from_str(&json).unwrap();
1259        assert_eq!(parsed, part);
1260        assert_eq!(parsed.as_text(), Some("hello"));
1261    }
1262
1263    #[test]
1264    fn part_file_uri_serialization() {
1265        let part = Part::file_uri("https://example.com/file.pdf", "application/pdf");
1266        let json = serde_json::to_string(&part).unwrap();
1267        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1268        assert_eq!(value.get("kind").unwrap().as_str().unwrap(), "file");
1269        let file = value.get("file").unwrap();
1270        assert_eq!(
1271            file.get("uri").unwrap().as_str().unwrap(),
1272            "https://example.com/file.pdf"
1273        );
1274        assert_eq!(
1275            file.get("mimeType").unwrap().as_str().unwrap(),
1276            "application/pdf"
1277        );
1278    }
1279
1280    #[test]
1281    fn part_data_serialization() {
1282        let part = Part::data(serde_json::json!({"key": "value"}));
1283        let json = serde_json::to_string(&part).unwrap();
1284        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1285        assert_eq!(value.get("kind").unwrap().as_str().unwrap(), "data");
1286        assert_eq!(
1287            value.get("data").unwrap(),
1288            &serde_json::json!({"key": "value"})
1289        );
1290    }
1291
1292    #[test]
1293    fn part_deserialization_from_wire_format() {
1294        // v1.0 flat format (no kind)
1295        let text: Part = serde_json::from_str(r#"{"text":"hello"}"#).unwrap();
1296        assert_eq!(text.as_text(), Some("hello"));
1297
1298        let file: Part = serde_json::from_str(
1299            r#"{"file":{"uri":"https://example.com/f.pdf","mimeType":"application/pdf"}}"#,
1300        )
1301        .unwrap();
1302        match &file {
1303            Part::File { file, .. } => {
1304                assert_eq!(file.uri.as_deref(), Some("https://example.com/f.pdf"));
1305                assert_eq!(file.mime_type.as_deref(), Some("application/pdf"));
1306            }
1307            _ => panic!("expected File part"),
1308        }
1309
1310        let data: Part = serde_json::from_str(r#"{"data":{"k":"v"}}"#).unwrap();
1311        match &data {
1312            Part::Data { data, .. } => assert_eq!(data, &serde_json::json!({"k": "v"})),
1313            _ => panic!("expected Data part"),
1314        }
1315
1316        // v0.3 kind-discriminated format (backward compat)
1317        let text_v03: Part =
1318            serde_json::from_str(r#"{"kind":"text","text":"hello v03"}"#).unwrap();
1319        assert_eq!(text_v03.as_text(), Some("hello v03"));
1320    }
1321
1322    #[test]
1323    fn agent_card_with_security() {
1324        let card = AgentCard {
1325            name: "Test Agent".to_string(),
1326            description: "Test description".to_string(),
1327            supported_interfaces: vec![AgentInterface {
1328                url: "https://example.com/v1/rpc".to_string(),
1329                protocol_binding: "JSONRPC".to_string(),
1330                protocol_version: PROTOCOL_VERSION.to_string(),
1331                tenant: None,
1332            }],
1333            provider: Some(AgentProvider {
1334                organization: "Test Org".to_string(),
1335                url: "https://example.com".to_string(),
1336            }),
1337            version: PROTOCOL_VERSION.to_string(),
1338            documentation_url: None,
1339            capabilities: AgentCapabilities::default(),
1340            security_schemes: {
1341                let mut m = HashMap::new();
1342                m.insert(
1343                    "apiKey".to_string(),
1344                    SecurityScheme::ApiKeySecurityScheme(ApiKeySecurityScheme {
1345                        name: "X-API-Key".to_string(),
1346                        location: "header".to_string(),
1347                        description: None,
1348                    }),
1349                );
1350                m
1351            },
1352            security_requirements: vec![],
1353            default_input_modes: vec![],
1354            default_output_modes: vec![],
1355            skills: vec![],
1356            signatures: vec![],
1357            icon_url: None,
1358        };
1359
1360        let json = serde_json::to_string(&card).unwrap();
1361        let parsed: AgentCard = serde_json::from_str(&json).unwrap();
1362        assert_eq!(parsed.name, "Test Agent");
1363        assert_eq!(parsed.security_schemes.len(), 1);
1364        assert_eq!(parsed.endpoint(), Some("https://example.com/v1/rpc"));
1365
1366        // Verify camelCase
1367        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1368        assert!(value.get("supportedInterfaces").is_some());
1369        assert!(value.get("securitySchemes").is_some());
1370        assert!(value.get("securityRequirements").is_some());
1371    }
1372
1373    #[test]
1374    fn validate_task_id_helper() {
1375        let valid_uuid = Uuid::new_v4().to_string();
1376        assert!(validate_task_id(&valid_uuid));
1377        assert!(!validate_task_id("not-a-uuid"));
1378    }
1379
1380    #[test]
1381    fn error_codes() {
1382        use errors::*;
1383        assert_eq!(message_for_code(TASK_NOT_FOUND), "Task not found");
1384        assert_eq!(
1385            message_for_code(VERSION_NOT_SUPPORTED),
1386            "Protocol version not supported"
1387        );
1388        assert_eq!(
1389            message_for_code(INVALID_AGENT_RESPONSE),
1390            "Invalid agent response"
1391        );
1392        assert_eq!(
1393            message_for_code(EXTENSION_SUPPORT_REQUIRED),
1394            "Extension support required"
1395        );
1396        assert_eq!(message_for_code(999), "Unknown error");
1397    }
1398
1399    #[test]
1400    fn send_message_result_serialization() {
1401        let task = Task {
1402            kind: "task".to_string(),
1403            id: "t-1".to_string(),
1404            context_id: "ctx-1".to_string(),
1405            status: TaskStatus {
1406                state: TaskState::Completed,
1407                message: None,
1408                timestamp: None,
1409            },
1410            artifacts: None,
1411            history: None,
1412            metadata: None,
1413        };
1414
1415        // SendMessageResult (wire format) uses externally tagged: {"task": {...}}
1416        let result = SendMessageResult::Task(task.clone());
1417        let json = serde_json::to_string(&result).unwrap();
1418        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1419        assert!(value.get("task").is_some(), "should have task wrapper key");
1420        let inner = value.get("task").unwrap();
1421        assert_eq!(inner.get("id").unwrap().as_str().unwrap(), "t-1");
1422
1423        // Round-trip
1424        let parsed: SendMessageResult = serde_json::from_str(&json).unwrap();
1425        assert_eq!(parsed, SendMessageResult::Task(task));
1426    }
1427
1428    #[test]
1429    fn stream_response_serialization() {
1430        let event = StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
1431            kind: "status-update".to_string(),
1432            task_id: "t-1".to_string(),
1433            context_id: "ctx-1".to_string(),
1434            status: TaskStatus {
1435                state: TaskState::Working,
1436                message: None,
1437                timestamp: None,
1438            },
1439            is_final: false,
1440            metadata: None,
1441        });
1442        let json = serde_json::to_string(&event).unwrap();
1443        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1444        assert!(value.get("statusUpdate").is_some());
1445    }
1446
1447    #[test]
1448    fn push_notification_config_serialization() {
1449        let config = PushNotificationConfig {
1450            id: Some("cfg-1".to_string()),
1451            url: "https://example.com/webhook".to_string(),
1452            token: Some("secret".to_string()),
1453            authentication: Some(AuthenticationInfo {
1454                scheme: "bearer".to_string(),
1455                credentials: Some("token123".to_string()),
1456            }),
1457        };
1458        let json = serde_json::to_string(&config).unwrap();
1459        let parsed: PushNotificationConfig = serde_json::from_str(&json).unwrap();
1460        assert_eq!(parsed.url, "https://example.com/webhook");
1461        assert_eq!(parsed.authentication.unwrap().scheme, "bearer");
1462    }
1463}