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