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 — serializes as v0.3 style ("user"/"agent") for broad compat,
536/// deserializes from both v0.3 and v1.0 protobuf style ("ROLE_USER"/"ROLE_AGENT").
537#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
538#[non_exhaustive]
539pub enum Role {
540    #[serde(rename = "unspecified", alias = "ROLE_UNSPECIFIED")]
541    Unspecified,
542    #[serde(rename = "user", alias = "ROLE_USER")]
543    User,
544    #[serde(rename = "agent", alias = "ROLE_AGENT")]
545    Agent,
546}
547
548/// Message structure per A2A RC 1.0 spec
549#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
550#[serde(rename_all = "camelCase")]
551pub struct Message {
552    /// Kind discriminator — always "message"
553    #[serde(default = "default_message_kind")]
554    pub kind: String,
555    /// Unique message identifier
556    pub message_id: String,
557    /// Optional conversation context ID
558    #[serde(default, skip_serializing_if = "Option::is_none")]
559    pub context_id: Option<String>,
560    /// Optional task reference
561    #[serde(default, skip_serializing_if = "Option::is_none")]
562    pub task_id: Option<String>,
563    /// Message role (user or agent)
564    pub role: Role,
565    /// Message content parts
566    pub parts: Vec<Part>,
567    /// Custom metadata
568    #[serde(default, skip_serializing_if = "Option::is_none")]
569    pub metadata: Option<serde_json::Value>,
570    /// Extension URIs
571    #[serde(default, skip_serializing_if = "Vec::is_empty", deserialize_with = "nullable_vec")]
572    pub extensions: Vec<String>,
573    /// Optional related task IDs
574    #[serde(default, skip_serializing_if = "Option::is_none")]
575    pub reference_task_ids: Option<Vec<String>>,
576}
577
578fn default_message_kind() -> String {
579    "message".to_string()
580}
581
582fn default_task_kind() -> String {
583    "task".to_string()
584}
585
586fn default_status_update_kind() -> String {
587    "status-update".to_string()
588}
589
590fn default_artifact_update_kind() -> String {
591    "artifact-update".to_string()
592}
593
594/// Artifact output from task processing per proto spec
595#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
596#[serde(rename_all = "camelCase")]
597pub struct Artifact {
598    /// Unique artifact identifier
599    pub artifact_id: String,
600    /// Optional display name
601    #[serde(default, skip_serializing_if = "Option::is_none")]
602    pub name: Option<String>,
603    /// Optional description
604    #[serde(default, skip_serializing_if = "Option::is_none")]
605    pub description: Option<String>,
606    /// Artifact content parts
607    pub parts: Vec<Part>,
608    /// Custom metadata
609    #[serde(default, skip_serializing_if = "Option::is_none")]
610    pub metadata: Option<serde_json::Value>,
611    /// Extension URIs
612    #[serde(default, skip_serializing_if = "Vec::is_empty", deserialize_with = "nullable_vec")]
613    pub extensions: Vec<String>,
614}
615
616/// Task lifecycle state — serializes as v0.3 style for broad compat,
617/// deserializes from both v0.3 and v1.0 protobuf style.
618#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
619#[non_exhaustive]
620pub enum TaskState {
621    #[serde(rename = "unspecified", alias = "TASK_STATE_UNSPECIFIED")]
622    Unspecified,
623    #[serde(rename = "submitted", alias = "TASK_STATE_SUBMITTED")]
624    Submitted,
625    #[serde(rename = "working", alias = "TASK_STATE_WORKING")]
626    Working,
627    #[serde(rename = "completed", alias = "TASK_STATE_COMPLETED")]
628    Completed,
629    #[serde(rename = "failed", alias = "TASK_STATE_FAILED")]
630    Failed,
631    #[serde(rename = "canceled", alias = "TASK_STATE_CANCELED")]
632    Canceled,
633    #[serde(rename = "input-required", alias = "TASK_STATE_INPUT_REQUIRED")]
634    InputRequired,
635    #[serde(rename = "rejected", alias = "TASK_STATE_REJECTED")]
636    Rejected,
637    #[serde(rename = "auth-required", alias = "TASK_STATE_AUTH_REQUIRED")]
638    AuthRequired,
639}
640
641/// Task status information
642#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
643#[serde(rename_all = "camelCase")]
644pub struct TaskStatus {
645    /// Current lifecycle state
646    pub state: TaskState,
647    /// Optional status message
648    #[serde(default, skip_serializing_if = "Option::is_none")]
649    pub message: Option<Message>,
650    /// ISO 8601 timestamp (e.g., "2023-10-27T10:00:00Z")
651    #[serde(default, skip_serializing_if = "Option::is_none")]
652    pub timestamp: Option<String>,
653}
654
655/// Task resource per A2A RC 1.0 spec
656#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
657#[serde(rename_all = "camelCase")]
658pub struct Task {
659    /// Kind discriminator — always "task"
660    #[serde(default = "default_task_kind")]
661    pub kind: String,
662    /// Unique task identifier (UUID)
663    pub id: String,
664    /// Context identifier for grouping related interactions
665    pub context_id: String,
666    /// Current task status
667    pub status: TaskStatus,
668    /// Optional output artifacts
669    #[serde(default, skip_serializing_if = "Option::is_none")]
670    pub artifacts: Option<Vec<Artifact>>,
671    /// Optional message history
672    #[serde(default, skip_serializing_if = "Option::is_none")]
673    pub history: Option<Vec<Message>>,
674    /// Custom metadata
675    #[serde(default, skip_serializing_if = "Option::is_none")]
676    pub metadata: Option<serde_json::Value>,
677}
678
679impl TaskState {
680    /// Check if state is terminal (no further updates expected)
681    pub fn is_terminal(&self) -> bool {
682        matches!(
683            self,
684            TaskState::Completed | TaskState::Failed | TaskState::Canceled | TaskState::Rejected
685        )
686    }
687}
688
689// ---------- SendMessage response ----------
690
691/// Handler-level response from SendMessage — can be a Task or direct Message.
692///
693/// Uses externally tagged serialization for internal pattern matching.
694/// For the wire format (JSON-RPC result field), use [`SendMessageResult`].
695#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
696#[serde(rename_all = "camelCase")]
697pub enum SendMessageResponse {
698    Task(Task),
699    Message(Message),
700}
701
702/// Wire-format result for message/send — the value inside the JSON-RPC `result` field.
703///
704/// Uses externally tagged serialization per v1.0 proto-JSON:
705/// - `{"task": {...}}` or `{"message": {...}}`
706#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
707#[serde(rename_all = "camelCase")]
708pub enum SendMessageResult {
709    Task(Task),
710    Message(Message),
711}
712
713// ---------- JSON-RPC helper types ----------
714
715#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
716pub struct JsonRpcRequest {
717    pub jsonrpc: String,
718    pub method: String,
719    #[serde(default)]
720    pub params: Option<serde_json::Value>,
721    pub id: serde_json::Value,
722}
723
724#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
725pub struct JsonRpcResponse {
726    pub jsonrpc: String,
727    pub id: serde_json::Value,
728    #[serde(skip_serializing_if = "Option::is_none")]
729    pub result: Option<serde_json::Value>,
730    #[serde(skip_serializing_if = "Option::is_none")]
731    pub error: Option<JsonRpcError>,
732}
733
734#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
735pub struct JsonRpcError {
736    pub code: i32,
737    pub message: String,
738    #[serde(skip_serializing_if = "Option::is_none")]
739    pub data: Option<serde_json::Value>,
740}
741
742/// JSON-RPC and A2A-specific error codes
743pub mod errors {
744    // Standard JSON-RPC 2.0 errors
745    pub const PARSE_ERROR: i32 = -32700;
746    pub const INVALID_REQUEST: i32 = -32600;
747    pub const METHOD_NOT_FOUND: i32 = -32601;
748    pub const INVALID_PARAMS: i32 = -32602;
749    pub const INTERNAL_ERROR: i32 = -32603;
750
751    // A2A-specific errors
752    pub const TASK_NOT_FOUND: i32 = -32001;
753    pub const TASK_NOT_CANCELABLE: i32 = -32002;
754    pub const PUSH_NOTIFICATION_NOT_SUPPORTED: i32 = -32003;
755    pub const UNSUPPORTED_OPERATION: i32 = -32004;
756    pub const CONTENT_TYPE_NOT_SUPPORTED: i32 = -32005;
757    pub const EXTENDED_AGENT_CARD_NOT_CONFIGURED: i32 = -32006;
758    pub const VERSION_NOT_SUPPORTED: i32 = -32007;
759    pub const INVALID_AGENT_RESPONSE: i32 = -32008;
760    pub const EXTENSION_SUPPORT_REQUIRED: i32 = -32009;
761
762    pub fn message_for_code(code: i32) -> &'static str {
763        match code {
764            PARSE_ERROR => "Parse error",
765            INVALID_REQUEST => "Invalid request",
766            METHOD_NOT_FOUND => "Method not found",
767            INVALID_PARAMS => "Invalid params",
768            INTERNAL_ERROR => "Internal error",
769            TASK_NOT_FOUND => "Task not found",
770            TASK_NOT_CANCELABLE => "Task not cancelable",
771            PUSH_NOTIFICATION_NOT_SUPPORTED => "Push notifications not supported",
772            UNSUPPORTED_OPERATION => "Unsupported operation",
773            CONTENT_TYPE_NOT_SUPPORTED => "Content type not supported",
774            EXTENDED_AGENT_CARD_NOT_CONFIGURED => "Extended agent card not configured",
775            VERSION_NOT_SUPPORTED => "Protocol version not supported",
776            INVALID_AGENT_RESPONSE => "Invalid agent response",
777            EXTENSION_SUPPORT_REQUIRED => "Extension support required",
778            _ => "Unknown error",
779        }
780    }
781}
782
783pub fn success(id: serde_json::Value, result: serde_json::Value) -> JsonRpcResponse {
784    JsonRpcResponse {
785        jsonrpc: "2.0".to_string(),
786        id,
787        result: Some(result),
788        error: None,
789    }
790}
791
792pub fn error(
793    id: serde_json::Value,
794    code: i32,
795    message: &str,
796    data: Option<serde_json::Value>,
797) -> JsonRpcResponse {
798    JsonRpcResponse {
799        jsonrpc: "2.0".to_string(),
800        id,
801        result: None,
802        error: Some(JsonRpcError {
803            code,
804            message: message.to_string(),
805            data,
806        }),
807    }
808}
809
810// ---------- Method params ----------
811
812/// Parameters for message/send operation
813#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
814#[serde(rename_all = "camelCase")]
815pub struct SendMessageRequest {
816    /// Optional tenant for multi-tenancy
817    #[serde(default, skip_serializing_if = "Option::is_none")]
818    pub tenant: Option<String>,
819    /// The message to send
820    pub message: Message,
821    /// Optional request configuration
822    #[serde(default, skip_serializing_if = "Option::is_none")]
823    pub configuration: Option<SendMessageConfiguration>,
824    /// Custom metadata
825    #[serde(default, skip_serializing_if = "Option::is_none")]
826    pub metadata: Option<serde_json::Value>,
827}
828
829/// Configuration for message send requests
830#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
831#[serde(rename_all = "camelCase")]
832pub struct SendMessageConfiguration {
833    /// Preferred output MIME types
834    #[serde(default, skip_serializing_if = "Option::is_none")]
835    pub accepted_output_modes: Option<Vec<String>>,
836    /// Push notification configuration for this request
837    #[serde(default, skip_serializing_if = "Option::is_none")]
838    pub push_notification_config: Option<PushNotificationConfig>,
839    /// Message history depth (0 = omit, None = server default)
840    #[serde(default, skip_serializing_if = "Option::is_none")]
841    pub history_length: Option<u32>,
842    /// Wait for task completion (default: false)
843    #[serde(default, skip_serializing_if = "Option::is_none")]
844    pub blocking: Option<bool>,
845    /// Return immediately without waiting for completion (default: false).
846    /// When true, the server returns the task in its current state even if non-terminal.
847    #[serde(default, skip_serializing_if = "Option::is_none")]
848    pub return_immediately: Option<bool>,
849}
850
851/// Parameters for tasks/get operation
852#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
853#[serde(rename_all = "camelCase")]
854pub struct GetTaskRequest {
855    /// Task ID
856    pub id: String,
857    /// Message history depth
858    #[serde(default, skip_serializing_if = "Option::is_none")]
859    pub history_length: Option<u32>,
860    /// Optional tenant
861    #[serde(default, skip_serializing_if = "Option::is_none")]
862    pub tenant: Option<String>,
863}
864
865/// Parameters for tasks/cancel operation
866#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
867#[serde(rename_all = "camelCase")]
868pub struct CancelTaskRequest {
869    /// Task ID
870    pub id: String,
871    /// Optional tenant
872    #[serde(default, skip_serializing_if = "Option::is_none")]
873    pub tenant: Option<String>,
874}
875
876/// Parameters for tasks/list operation
877#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
878#[serde(rename_all = "camelCase")]
879pub struct ListTasksRequest {
880    /// Filter by context ID
881    #[serde(default, skip_serializing_if = "Option::is_none")]
882    pub context_id: Option<String>,
883    /// Filter by task state
884    #[serde(default, skip_serializing_if = "Option::is_none")]
885    pub status: Option<TaskState>,
886    /// Results per page (1-100, default 50)
887    #[serde(default, skip_serializing_if = "Option::is_none")]
888    pub page_size: Option<u32>,
889    /// Pagination cursor
890    #[serde(default, skip_serializing_if = "Option::is_none")]
891    pub page_token: Option<String>,
892    /// History depth per task
893    #[serde(default, skip_serializing_if = "Option::is_none")]
894    pub history_length: Option<u32>,
895    /// Filter by status timestamp after (ISO 8601 or millis)
896    #[serde(default, skip_serializing_if = "Option::is_none")]
897    pub status_timestamp_after: Option<i64>,
898    /// Include artifacts in response
899    #[serde(default, skip_serializing_if = "Option::is_none")]
900    pub include_artifacts: Option<bool>,
901    /// Optional tenant
902    #[serde(default, skip_serializing_if = "Option::is_none")]
903    pub tenant: Option<String>,
904}
905
906/// Response for tasks/list operation
907#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
908#[serde(rename_all = "camelCase")]
909pub struct TaskListResponse {
910    /// Tasks matching the query
911    pub tasks: Vec<Task>,
912    /// Empty string if this is the final page
913    pub next_page_token: String,
914    /// Requested page size
915    pub page_size: u32,
916    /// Total matching tasks
917    pub total_size: u32,
918}
919
920/// Parameters for tasks/subscribe operation
921#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
922#[serde(rename_all = "camelCase")]
923pub struct SubscribeToTaskRequest {
924    /// Task ID
925    pub id: String,
926    /// Optional tenant
927    #[serde(default, skip_serializing_if = "Option::is_none")]
928    pub tenant: Option<String>,
929}
930
931/// Push notification configuration per proto spec
932#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
933#[serde(rename_all = "camelCase")]
934pub struct PushNotificationConfig {
935    /// Configuration identifier
936    #[serde(default, skip_serializing_if = "Option::is_none")]
937    pub id: Option<String>,
938    /// Webhook URL to receive notifications
939    pub url: String,
940    /// Token for webhook authentication
941    #[serde(default, skip_serializing_if = "Option::is_none")]
942    pub token: Option<String>,
943    /// Authentication details for webhook delivery
944    #[serde(default, skip_serializing_if = "Option::is_none")]
945    pub authentication: Option<AuthenticationInfo>,
946}
947
948/// Authentication info for push notification delivery per proto spec
949#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
950#[serde(rename_all = "camelCase")]
951pub struct AuthenticationInfo {
952    /// Auth scheme (e.g., "bearer", "api_key")
953    pub scheme: String,
954    /// Credentials (e.g., token value) — optional in RC 1.0
955    #[serde(default, skip_serializing_if = "Option::is_none")]
956    pub credentials: Option<String>,
957}
958
959/// Wrapper for push notification config with task context
960#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
961#[serde(rename_all = "camelCase")]
962pub struct TaskPushNotificationConfig {
963    /// Configuration ID
964    pub id: String,
965    /// Associated task ID
966    pub task_id: String,
967    /// The push notification configuration
968    pub push_notification_config: PushNotificationConfig,
969    /// Optional tenant
970    #[serde(default, skip_serializing_if = "Option::is_none")]
971    pub tenant: Option<String>,
972}
973
974/// Parameters for tasks/pushNotificationConfig/create
975#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
976#[serde(rename_all = "camelCase")]
977pub struct CreateTaskPushNotificationConfigRequest {
978    /// Task ID
979    pub task_id: String,
980    /// Configuration identifier
981    pub config_id: String,
982    /// Configuration details
983    pub push_notification_config: PushNotificationConfig,
984    /// Optional tenant
985    #[serde(default, skip_serializing_if = "Option::is_none")]
986    pub tenant: Option<String>,
987}
988
989/// Parameters for tasks/pushNotificationConfig/get
990#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
991#[serde(rename_all = "camelCase")]
992pub struct GetTaskPushNotificationConfigRequest {
993    /// Config ID
994    pub id: String,
995    /// Task ID
996    pub task_id: String,
997    /// Optional tenant
998    #[serde(default, skip_serializing_if = "Option::is_none")]
999    pub tenant: Option<String>,
1000}
1001
1002/// Parameters for tasks/pushNotificationConfig/list
1003#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1004#[serde(rename_all = "camelCase")]
1005pub struct ListTaskPushNotificationConfigRequest {
1006    /// Task ID
1007    pub task_id: String,
1008    /// Max configs to return
1009    #[serde(default, skip_serializing_if = "Option::is_none")]
1010    pub page_size: Option<u32>,
1011    /// Pagination cursor
1012    #[serde(default, skip_serializing_if = "Option::is_none")]
1013    pub page_token: Option<String>,
1014    /// Optional tenant
1015    #[serde(default, skip_serializing_if = "Option::is_none")]
1016    pub tenant: Option<String>,
1017}
1018
1019/// Response for tasks/pushNotificationConfig/list
1020#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1021#[serde(rename_all = "camelCase")]
1022pub struct ListTaskPushNotificationConfigResponse {
1023    /// Push notification configurations
1024    pub configs: Vec<TaskPushNotificationConfig>,
1025    /// Next page token
1026    #[serde(default)]
1027    pub next_page_token: String,
1028}
1029
1030/// Parameters for tasks/pushNotificationConfig/delete
1031#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1032#[serde(rename_all = "camelCase")]
1033pub struct DeleteTaskPushNotificationConfigRequest {
1034    /// Config ID
1035    pub id: String,
1036    /// Task ID
1037    pub task_id: String,
1038    /// Optional tenant
1039    #[serde(default, skip_serializing_if = "Option::is_none")]
1040    pub tenant: Option<String>,
1041}
1042
1043/// Parameters for agentCard/getExtended
1044#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1045#[serde(rename_all = "camelCase")]
1046pub struct GetExtendedAgentCardRequest {
1047    /// Optional tenant
1048    #[serde(default, skip_serializing_if = "Option::is_none")]
1049    pub tenant: Option<String>,
1050}
1051
1052// ---------- Streaming event types ----------
1053
1054/// Streaming response per proto spec — uses externally tagged oneof
1055#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1056#[serde(rename_all = "camelCase")]
1057pub enum StreamResponse {
1058    /// Complete task snapshot
1059    Task(Task),
1060    /// Direct message response
1061    Message(Message),
1062    /// Task status update event
1063    StatusUpdate(TaskStatusUpdateEvent),
1064    /// Task artifact update event
1065    ArtifactUpdate(TaskArtifactUpdateEvent),
1066}
1067
1068/// Task status update event
1069#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1070#[serde(rename_all = "camelCase")]
1071pub struct TaskStatusUpdateEvent {
1072    /// Kind discriminator — always "status-update"
1073    #[serde(default = "default_status_update_kind")]
1074    pub kind: String,
1075    /// Task identifier
1076    pub task_id: String,
1077    /// Context identifier
1078    pub context_id: String,
1079    /// Updated status
1080    pub status: TaskStatus,
1081    /// Whether this is the final event in the stream
1082    #[serde(rename = "final", default)]
1083    pub is_final: bool,
1084    /// Custom metadata
1085    #[serde(default, skip_serializing_if = "Option::is_none")]
1086    pub metadata: Option<serde_json::Value>,
1087}
1088
1089/// Task artifact update event
1090#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1091#[serde(rename_all = "camelCase")]
1092pub struct TaskArtifactUpdateEvent {
1093    /// Kind discriminator — always "artifact-update"
1094    #[serde(default = "default_artifact_update_kind")]
1095    pub kind: String,
1096    /// Task identifier
1097    pub task_id: String,
1098    /// Context identifier
1099    pub context_id: String,
1100    /// New or updated artifact
1101    pub artifact: Artifact,
1102    /// Whether to append to existing artifact
1103    #[serde(default, skip_serializing_if = "Option::is_none")]
1104    pub append: Option<bool>,
1105    /// Whether this is the last chunk
1106    #[serde(default, skip_serializing_if = "Option::is_none")]
1107    pub last_chunk: Option<bool>,
1108    /// Custom metadata
1109    #[serde(default, skip_serializing_if = "Option::is_none")]
1110    pub metadata: Option<serde_json::Value>,
1111}
1112
1113/// Wire-format result for streaming events — the value inside each SSE JSON-RPC `result` field.
1114///
1115/// Uses externally tagged serialization per v1.0 proto-JSON:
1116/// - `{"statusUpdate": {...}}`, `{"artifactUpdate": {...}}`, `{"task": {...}}`, `{"message": {...}}`
1117#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1118#[serde(rename_all = "camelCase")]
1119pub enum StreamingMessageResult {
1120    StatusUpdate(TaskStatusUpdateEvent),
1121    ArtifactUpdate(TaskArtifactUpdateEvent),
1122    Task(Task),
1123    Message(Message),
1124}
1125
1126// ---------- Helper functions ----------
1127
1128/// Create a new message with text content
1129pub fn new_message(role: Role, text: &str, context_id: Option<String>) -> Message {
1130    Message {
1131        kind: "message".to_string(),
1132        message_id: Uuid::new_v4().to_string(),
1133        context_id,
1134        task_id: None,
1135        role,
1136        parts: vec![Part::text(text)],
1137        metadata: None,
1138        extensions: vec![],
1139        reference_task_ids: None,
1140    }
1141}
1142
1143/// Create a completed task with text response
1144pub fn completed_task_with_text(user_message: Message, reply_text: &str) -> Task {
1145    let context_id = user_message
1146        .context_id
1147        .clone()
1148        .unwrap_or_else(|| Uuid::new_v4().to_string());
1149    let task_id = Uuid::new_v4().to_string();
1150    let agent_msg = new_message(Role::Agent, reply_text, Some(context_id.clone()));
1151
1152    Task {
1153        kind: "task".to_string(),
1154        id: task_id,
1155        context_id,
1156        status: TaskStatus {
1157            state: TaskState::Completed,
1158            message: Some(agent_msg.clone()),
1159            timestamp: Some(chrono::Utc::now().to_rfc3339()),
1160        },
1161        history: Some(vec![user_message, agent_msg]),
1162        artifacts: None,
1163        metadata: None,
1164    }
1165}
1166
1167/// Generate ISO 8601 timestamp
1168pub fn now_iso8601() -> String {
1169    chrono::Utc::now().to_rfc3339()
1170}
1171
1172/// Validate task ID format (UUID)
1173pub fn validate_task_id(id: &str) -> bool {
1174    Uuid::parse_str(id).is_ok()
1175}
1176
1177#[cfg(test)]
1178mod tests {
1179    use super::*;
1180
1181    #[test]
1182    fn jsonrpc_helpers_round_trip() {
1183        let resp = success(serde_json::json!(1), serde_json::json!({"ok": true}));
1184        assert_eq!(resp.jsonrpc, "2.0");
1185        assert!(resp.error.is_none());
1186        assert!(resp.result.is_some());
1187    }
1188
1189    #[test]
1190    fn task_state_is_terminal() {
1191        assert!(TaskState::Completed.is_terminal());
1192        assert!(TaskState::Failed.is_terminal());
1193        assert!(TaskState::Canceled.is_terminal());
1194        assert!(TaskState::Rejected.is_terminal());
1195        assert!(!TaskState::Working.is_terminal());
1196        assert!(!TaskState::Submitted.is_terminal());
1197        assert!(!TaskState::InputRequired.is_terminal());
1198    }
1199
1200    #[test]
1201    fn task_state_serialization() {
1202        let state = TaskState::Working;
1203        let json = serde_json::to_string(&state).unwrap();
1204        assert_eq!(json, r#""working""#);
1205
1206        // v0.3 style round-trips
1207        let parsed: TaskState = serde_json::from_str(&json).unwrap();
1208        assert_eq!(parsed, TaskState::Working);
1209
1210        // v1.0 protobuf style still deserializes via alias
1211        let parsed: TaskState = serde_json::from_str(r#""TASK_STATE_WORKING""#).unwrap();
1212        assert_eq!(parsed, TaskState::Working);
1213    }
1214
1215    #[test]
1216    fn role_serialization() {
1217        let role = Role::User;
1218        let json = serde_json::to_string(&role).unwrap();
1219        assert_eq!(json, r#""user""#);
1220
1221        // v1.0 protobuf style still deserializes via alias
1222        let parsed: Role = serde_json::from_str(r#""ROLE_USER""#).unwrap();
1223        assert_eq!(parsed, Role::User);
1224    }
1225
1226    #[test]
1227    fn message_serialization() {
1228        let msg = new_message(Role::User, "hello", Some("ctx-123".to_string()));
1229        let json = serde_json::to_string(&msg).unwrap();
1230        let parsed: Message = serde_json::from_str(&json).unwrap();
1231        assert_eq!(parsed.role, Role::User);
1232        assert_eq!(parsed.parts.len(), 1);
1233        assert_eq!(parsed.parts[0].as_text(), Some("hello"));
1234
1235        // Verify camelCase field names
1236        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1237        assert!(value.get("messageId").is_some());
1238        assert!(value.get("contextId").is_some());
1239    }
1240
1241    #[test]
1242    fn task_serialization() {
1243        let user_msg = new_message(Role::User, "test", None);
1244        let task = completed_task_with_text(user_msg, "response");
1245        let json = serde_json::to_string(&task).unwrap();
1246        let parsed: Task = serde_json::from_str(&json).unwrap();
1247        assert_eq!(parsed.status.state, TaskState::Completed);
1248        assert!(parsed.history.is_some());
1249        assert_eq!(parsed.history.unwrap().len(), 2);
1250
1251        // Verify camelCase
1252        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1253        assert!(value.get("contextId").is_some());
1254    }
1255
1256    #[test]
1257    fn part_text_serialization() {
1258        let part = Part::text("hello");
1259        let json = serde_json::to_string(&part).unwrap();
1260        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1261        assert_eq!(value.get("kind").unwrap().as_str().unwrap(), "text");
1262        assert_eq!(value.get("text").unwrap().as_str().unwrap(), "hello");
1263    }
1264
1265    #[test]
1266    fn part_text_round_trip() {
1267        let part = Part::text("hello");
1268        let json = serde_json::to_string(&part).unwrap();
1269        let parsed: Part = serde_json::from_str(&json).unwrap();
1270        assert_eq!(parsed, part);
1271        assert_eq!(parsed.as_text(), Some("hello"));
1272    }
1273
1274    #[test]
1275    fn part_file_uri_serialization() {
1276        let part = Part::file_uri("https://example.com/file.pdf", "application/pdf");
1277        let json = serde_json::to_string(&part).unwrap();
1278        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1279        assert_eq!(value.get("kind").unwrap().as_str().unwrap(), "file");
1280        let file = value.get("file").unwrap();
1281        assert_eq!(
1282            file.get("uri").unwrap().as_str().unwrap(),
1283            "https://example.com/file.pdf"
1284        );
1285        assert_eq!(
1286            file.get("mimeType").unwrap().as_str().unwrap(),
1287            "application/pdf"
1288        );
1289    }
1290
1291    #[test]
1292    fn part_data_serialization() {
1293        let part = Part::data(serde_json::json!({"key": "value"}));
1294        let json = serde_json::to_string(&part).unwrap();
1295        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1296        assert_eq!(value.get("kind").unwrap().as_str().unwrap(), "data");
1297        assert_eq!(
1298            value.get("data").unwrap(),
1299            &serde_json::json!({"key": "value"})
1300        );
1301    }
1302
1303    #[test]
1304    fn part_deserialization_from_wire_format() {
1305        // v1.0 flat format (no kind)
1306        let text: Part = serde_json::from_str(r#"{"text":"hello"}"#).unwrap();
1307        assert_eq!(text.as_text(), Some("hello"));
1308
1309        let file: Part = serde_json::from_str(
1310            r#"{"file":{"uri":"https://example.com/f.pdf","mimeType":"application/pdf"}}"#,
1311        )
1312        .unwrap();
1313        match &file {
1314            Part::File { file, .. } => {
1315                assert_eq!(file.uri.as_deref(), Some("https://example.com/f.pdf"));
1316                assert_eq!(file.mime_type.as_deref(), Some("application/pdf"));
1317            }
1318            _ => panic!("expected File part"),
1319        }
1320
1321        let data: Part = serde_json::from_str(r#"{"data":{"k":"v"}}"#).unwrap();
1322        match &data {
1323            Part::Data { data, .. } => assert_eq!(data, &serde_json::json!({"k": "v"})),
1324            _ => panic!("expected Data part"),
1325        }
1326
1327        // v0.3 kind-discriminated format (backward compat)
1328        let text_v03: Part =
1329            serde_json::from_str(r#"{"kind":"text","text":"hello v03"}"#).unwrap();
1330        assert_eq!(text_v03.as_text(), Some("hello v03"));
1331    }
1332
1333    #[test]
1334    fn agent_card_with_security() {
1335        let card = AgentCard {
1336            name: "Test Agent".to_string(),
1337            description: "Test description".to_string(),
1338            supported_interfaces: vec![AgentInterface {
1339                url: "https://example.com/v1/rpc".to_string(),
1340                protocol_binding: "JSONRPC".to_string(),
1341                protocol_version: PROTOCOL_VERSION.to_string(),
1342                tenant: None,
1343            }],
1344            provider: Some(AgentProvider {
1345                organization: "Test Org".to_string(),
1346                url: "https://example.com".to_string(),
1347            }),
1348            version: PROTOCOL_VERSION.to_string(),
1349            documentation_url: None,
1350            capabilities: AgentCapabilities::default(),
1351            security_schemes: {
1352                let mut m = HashMap::new();
1353                m.insert(
1354                    "apiKey".to_string(),
1355                    SecurityScheme::ApiKeySecurityScheme(ApiKeySecurityScheme {
1356                        name: "X-API-Key".to_string(),
1357                        location: "header".to_string(),
1358                        description: None,
1359                    }),
1360                );
1361                m
1362            },
1363            security_requirements: vec![],
1364            default_input_modes: vec![],
1365            default_output_modes: vec![],
1366            skills: vec![],
1367            signatures: vec![],
1368            icon_url: None,
1369        };
1370
1371        let json = serde_json::to_string(&card).unwrap();
1372        let parsed: AgentCard = serde_json::from_str(&json).unwrap();
1373        assert_eq!(parsed.name, "Test Agent");
1374        assert_eq!(parsed.security_schemes.len(), 1);
1375        assert_eq!(parsed.endpoint(), Some("https://example.com/v1/rpc"));
1376
1377        // Verify camelCase
1378        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1379        assert!(value.get("supportedInterfaces").is_some());
1380        assert!(value.get("securitySchemes").is_some());
1381        assert!(value.get("securityRequirements").is_some());
1382    }
1383
1384    #[test]
1385    fn validate_task_id_helper() {
1386        let valid_uuid = Uuid::new_v4().to_string();
1387        assert!(validate_task_id(&valid_uuid));
1388        assert!(!validate_task_id("not-a-uuid"));
1389    }
1390
1391    #[test]
1392    fn error_codes() {
1393        use errors::*;
1394        assert_eq!(message_for_code(TASK_NOT_FOUND), "Task not found");
1395        assert_eq!(
1396            message_for_code(VERSION_NOT_SUPPORTED),
1397            "Protocol version not supported"
1398        );
1399        assert_eq!(
1400            message_for_code(INVALID_AGENT_RESPONSE),
1401            "Invalid agent response"
1402        );
1403        assert_eq!(
1404            message_for_code(EXTENSION_SUPPORT_REQUIRED),
1405            "Extension support required"
1406        );
1407        assert_eq!(message_for_code(999), "Unknown error");
1408    }
1409
1410    #[test]
1411    fn send_message_result_serialization() {
1412        let task = Task {
1413            kind: "task".to_string(),
1414            id: "t-1".to_string(),
1415            context_id: "ctx-1".to_string(),
1416            status: TaskStatus {
1417                state: TaskState::Completed,
1418                message: None,
1419                timestamp: None,
1420            },
1421            artifacts: None,
1422            history: None,
1423            metadata: None,
1424        };
1425
1426        // SendMessageResult (wire format) uses externally tagged: {"task": {...}}
1427        let result = SendMessageResult::Task(task.clone());
1428        let json = serde_json::to_string(&result).unwrap();
1429        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1430        assert!(value.get("task").is_some(), "should have task wrapper key");
1431        let inner = value.get("task").unwrap();
1432        assert_eq!(inner.get("id").unwrap().as_str().unwrap(), "t-1");
1433
1434        // Round-trip
1435        let parsed: SendMessageResult = serde_json::from_str(&json).unwrap();
1436        assert_eq!(parsed, SendMessageResult::Task(task));
1437    }
1438
1439    #[test]
1440    fn stream_response_serialization() {
1441        let event = StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
1442            kind: "status-update".to_string(),
1443            task_id: "t-1".to_string(),
1444            context_id: "ctx-1".to_string(),
1445            status: TaskStatus {
1446                state: TaskState::Working,
1447                message: None,
1448                timestamp: None,
1449            },
1450            is_final: false,
1451            metadata: None,
1452        });
1453        let json = serde_json::to_string(&event).unwrap();
1454        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1455        assert!(value.get("statusUpdate").is_some());
1456    }
1457
1458    #[test]
1459    fn push_notification_config_serialization() {
1460        let config = PushNotificationConfig {
1461            id: Some("cfg-1".to_string()),
1462            url: "https://example.com/webhook".to_string(),
1463            token: Some("secret".to_string()),
1464            authentication: Some(AuthenticationInfo {
1465                scheme: "bearer".to_string(),
1466                credentials: Some("token123".to_string()),
1467            }),
1468        };
1469        let json = serde_json::to_string(&config).unwrap();
1470        let parsed: PushNotificationConfig = serde_json::from_str(&json).unwrap();
1471        assert_eq!(parsed.url, "https://example.com/webhook");
1472        assert_eq!(parsed.authentication.unwrap().scheme, "bearer");
1473    }
1474}