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