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 — internally tagged enum using `kind` as the discriminator.
301///
302/// Matches the A2A reference SDK wire format:
303/// - `{"kind": "text", "text": "..."}`
304/// - `{"kind": "file", "file": {"uri": "...", "mimeType": "..."}}`
305/// - `{"kind": "data", "data": {...}}`
306#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
307#[serde(tag = "kind")]
308pub enum Part {
309    /// Text content part
310    #[serde(rename = "text")]
311    Text {
312        /// The text content
313        text: String,
314        /// Part-level metadata
315        #[serde(default, skip_serializing_if = "Option::is_none")]
316        metadata: Option<serde_json::Value>,
317    },
318    /// File content part (inline bytes or URI reference)
319    #[serde(rename = "file")]
320    File {
321        /// File content
322        file: FileContent,
323        /// Part-level metadata
324        #[serde(default, skip_serializing_if = "Option::is_none")]
325        metadata: Option<serde_json::Value>,
326    },
327    /// Structured data part
328    #[serde(rename = "data")]
329    Data {
330        /// Structured JSON data
331        data: serde_json::Value,
332        /// Part-level metadata
333        #[serde(default, skip_serializing_if = "Option::is_none")]
334        metadata: Option<serde_json::Value>,
335    },
336}
337
338/// File content — either inline bytes or a URI reference.
339#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
340#[serde(rename_all = "camelCase")]
341pub struct FileContent {
342    /// Base64-encoded file bytes
343    #[serde(default, skip_serializing_if = "Option::is_none")]
344    pub bytes: Option<String>,
345    /// URI pointing to the file
346    #[serde(default, skip_serializing_if = "Option::is_none")]
347    pub uri: Option<String>,
348    /// File name
349    #[serde(default, skip_serializing_if = "Option::is_none")]
350    pub name: Option<String>,
351    /// MIME type
352    #[serde(default, skip_serializing_if = "Option::is_none")]
353    pub mime_type: Option<String>,
354}
355
356impl Part {
357    /// Create a text part
358    pub fn text(text: impl Into<String>) -> Self {
359        Part::Text {
360            text: text.into(),
361            metadata: None,
362        }
363    }
364
365    /// Create a file part with a URI reference
366    pub fn file_uri(uri: impl Into<String>, mime_type: impl Into<String>) -> Self {
367        Part::File {
368            file: FileContent {
369                bytes: None,
370                uri: Some(uri.into()),
371                name: None,
372                mime_type: Some(mime_type.into()),
373            },
374            metadata: None,
375        }
376    }
377
378    /// Create a file part with inline bytes (base64-encoded)
379    pub fn file_bytes(bytes: impl Into<String>, mime_type: impl Into<String>) -> Self {
380        Part::File {
381            file: FileContent {
382                bytes: Some(bytes.into()),
383                uri: None,
384                name: None,
385                mime_type: Some(mime_type.into()),
386            },
387            metadata: None,
388        }
389    }
390
391    /// Create a structured data part
392    pub fn data(data: serde_json::Value) -> Self {
393        Part::Data {
394            data,
395            metadata: None,
396        }
397    }
398
399    /// Get the text content, if this is a text part
400    pub fn as_text(&self) -> Option<&str> {
401        match self {
402            Part::Text { text, .. } => Some(text),
403            _ => None,
404        }
405    }
406}
407
408// ---------- Messages, Tasks, Artifacts ----------
409
410/// Message role per proto spec
411#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
412#[non_exhaustive]
413pub enum Role {
414    #[serde(rename = "unspecified")]
415    Unspecified,
416    #[serde(rename = "user")]
417    User,
418    #[serde(rename = "agent")]
419    Agent,
420}
421
422/// Message structure per A2A RC 1.0 spec
423#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
424#[serde(rename_all = "camelCase")]
425pub struct Message {
426    /// Kind discriminator — always "message"
427    #[serde(default = "default_message_kind")]
428    pub kind: String,
429    /// Unique message identifier
430    pub message_id: String,
431    /// Optional conversation context ID
432    #[serde(default, skip_serializing_if = "Option::is_none")]
433    pub context_id: Option<String>,
434    /// Optional task reference
435    #[serde(default, skip_serializing_if = "Option::is_none")]
436    pub task_id: Option<String>,
437    /// Message role (user or agent)
438    pub role: Role,
439    /// Message content parts
440    pub parts: Vec<Part>,
441    /// Custom metadata
442    #[serde(default, skip_serializing_if = "Option::is_none")]
443    pub metadata: Option<serde_json::Value>,
444    /// Extension URIs
445    #[serde(default, skip_serializing_if = "Vec::is_empty")]
446    pub extensions: Vec<String>,
447    /// Optional related task IDs
448    #[serde(default, skip_serializing_if = "Option::is_none")]
449    pub reference_task_ids: Option<Vec<String>>,
450}
451
452fn default_message_kind() -> String {
453    "message".to_string()
454}
455
456fn default_task_kind() -> String {
457    "task".to_string()
458}
459
460fn default_status_update_kind() -> String {
461    "status-update".to_string()
462}
463
464fn default_artifact_update_kind() -> String {
465    "artifact-update".to_string()
466}
467
468/// Artifact output from task processing per proto spec
469#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
470#[serde(rename_all = "camelCase")]
471pub struct Artifact {
472    /// Unique artifact identifier
473    pub artifact_id: String,
474    /// Optional display name
475    #[serde(default, skip_serializing_if = "Option::is_none")]
476    pub name: Option<String>,
477    /// Optional description
478    #[serde(default, skip_serializing_if = "Option::is_none")]
479    pub description: Option<String>,
480    /// Artifact content parts
481    pub parts: Vec<Part>,
482    /// Custom metadata
483    #[serde(default, skip_serializing_if = "Option::is_none")]
484    pub metadata: Option<serde_json::Value>,
485    /// Extension URIs
486    #[serde(default, skip_serializing_if = "Vec::is_empty")]
487    pub extensions: Vec<String>,
488}
489
490/// Task lifecycle state per proto spec
491#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
492#[non_exhaustive]
493pub enum TaskState {
494    #[serde(rename = "unspecified")]
495    Unspecified,
496    #[serde(rename = "submitted")]
497    Submitted,
498    #[serde(rename = "working")]
499    Working,
500    #[serde(rename = "completed")]
501    Completed,
502    #[serde(rename = "failed")]
503    Failed,
504    #[serde(rename = "canceled")]
505    Canceled,
506    #[serde(rename = "input-required")]
507    InputRequired,
508    #[serde(rename = "rejected")]
509    Rejected,
510    #[serde(rename = "auth-required")]
511    AuthRequired,
512}
513
514/// Task status information
515#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
516#[serde(rename_all = "camelCase")]
517pub struct TaskStatus {
518    /// Current lifecycle state
519    pub state: TaskState,
520    /// Optional status message
521    #[serde(default, skip_serializing_if = "Option::is_none")]
522    pub message: Option<Message>,
523    /// ISO 8601 timestamp (e.g., "2023-10-27T10:00:00Z")
524    #[serde(default, skip_serializing_if = "Option::is_none")]
525    pub timestamp: Option<String>,
526}
527
528/// Task resource per A2A RC 1.0 spec
529#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
530#[serde(rename_all = "camelCase")]
531pub struct Task {
532    /// Kind discriminator — always "task"
533    #[serde(default = "default_task_kind")]
534    pub kind: String,
535    /// Unique task identifier (UUID)
536    pub id: String,
537    /// Context identifier for grouping related interactions
538    pub context_id: String,
539    /// Current task status
540    pub status: TaskStatus,
541    /// Optional output artifacts
542    #[serde(default, skip_serializing_if = "Option::is_none")]
543    pub artifacts: Option<Vec<Artifact>>,
544    /// Optional message history
545    #[serde(default, skip_serializing_if = "Option::is_none")]
546    pub history: Option<Vec<Message>>,
547    /// Custom metadata
548    #[serde(default, skip_serializing_if = "Option::is_none")]
549    pub metadata: Option<serde_json::Value>,
550}
551
552impl TaskState {
553    /// Check if state is terminal (no further updates expected)
554    pub fn is_terminal(&self) -> bool {
555        matches!(
556            self,
557            TaskState::Completed | TaskState::Failed | TaskState::Canceled | TaskState::Rejected
558        )
559    }
560}
561
562// ---------- SendMessage response ----------
563
564/// Handler-level response from SendMessage — can be a Task or direct Message.
565///
566/// Uses externally tagged serialization for internal pattern matching.
567/// For the wire format (JSON-RPC result field), use [`SendMessageResult`].
568#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
569#[serde(rename_all = "camelCase")]
570pub enum SendMessageResponse {
571    Task(Task),
572    Message(Message),
573}
574
575/// Wire-format result for message/send — the value inside the JSON-RPC `result` field.
576///
577/// Uses untagged serialization to match the A2A reference SDK, which puts
578/// the Task or Message object directly in the result field without a wrapper key.
579#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
580#[serde(untagged)]
581pub enum SendMessageResult {
582    Task(Task),
583    Message(Message),
584}
585
586// ---------- JSON-RPC helper types ----------
587
588#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
589pub struct JsonRpcRequest {
590    pub jsonrpc: String,
591    pub method: String,
592    #[serde(default)]
593    pub params: Option<serde_json::Value>,
594    pub id: serde_json::Value,
595}
596
597#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
598pub struct JsonRpcResponse {
599    pub jsonrpc: String,
600    pub id: serde_json::Value,
601    #[serde(skip_serializing_if = "Option::is_none")]
602    pub result: Option<serde_json::Value>,
603    #[serde(skip_serializing_if = "Option::is_none")]
604    pub error: Option<JsonRpcError>,
605}
606
607#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
608pub struct JsonRpcError {
609    pub code: i32,
610    pub message: String,
611    #[serde(skip_serializing_if = "Option::is_none")]
612    pub data: Option<serde_json::Value>,
613}
614
615/// JSON-RPC and A2A-specific error codes
616pub mod errors {
617    // Standard JSON-RPC 2.0 errors
618    pub const PARSE_ERROR: i32 = -32700;
619    pub const INVALID_REQUEST: i32 = -32600;
620    pub const METHOD_NOT_FOUND: i32 = -32601;
621    pub const INVALID_PARAMS: i32 = -32602;
622    pub const INTERNAL_ERROR: i32 = -32603;
623
624    // A2A-specific errors
625    pub const TASK_NOT_FOUND: i32 = -32001;
626    pub const TASK_NOT_CANCELABLE: i32 = -32002;
627    pub const PUSH_NOTIFICATION_NOT_SUPPORTED: i32 = -32003;
628    pub const UNSUPPORTED_OPERATION: i32 = -32004;
629    pub const CONTENT_TYPE_NOT_SUPPORTED: i32 = -32005;
630    pub const EXTENDED_AGENT_CARD_NOT_CONFIGURED: i32 = -32006;
631    pub const VERSION_NOT_SUPPORTED: i32 = -32007;
632    pub const INVALID_AGENT_RESPONSE: i32 = -32008;
633    pub const EXTENSION_SUPPORT_REQUIRED: i32 = -32009;
634
635    pub fn message_for_code(code: i32) -> &'static str {
636        match code {
637            PARSE_ERROR => "Parse error",
638            INVALID_REQUEST => "Invalid request",
639            METHOD_NOT_FOUND => "Method not found",
640            INVALID_PARAMS => "Invalid params",
641            INTERNAL_ERROR => "Internal error",
642            TASK_NOT_FOUND => "Task not found",
643            TASK_NOT_CANCELABLE => "Task not cancelable",
644            PUSH_NOTIFICATION_NOT_SUPPORTED => "Push notifications not supported",
645            UNSUPPORTED_OPERATION => "Unsupported operation",
646            CONTENT_TYPE_NOT_SUPPORTED => "Content type not supported",
647            EXTENDED_AGENT_CARD_NOT_CONFIGURED => "Extended agent card not configured",
648            VERSION_NOT_SUPPORTED => "Protocol version not supported",
649            INVALID_AGENT_RESPONSE => "Invalid agent response",
650            EXTENSION_SUPPORT_REQUIRED => "Extension support required",
651            _ => "Unknown error",
652        }
653    }
654}
655
656pub fn success(id: serde_json::Value, result: serde_json::Value) -> JsonRpcResponse {
657    JsonRpcResponse {
658        jsonrpc: "2.0".to_string(),
659        id,
660        result: Some(result),
661        error: None,
662    }
663}
664
665pub fn error(
666    id: serde_json::Value,
667    code: i32,
668    message: &str,
669    data: Option<serde_json::Value>,
670) -> JsonRpcResponse {
671    JsonRpcResponse {
672        jsonrpc: "2.0".to_string(),
673        id,
674        result: None,
675        error: Some(JsonRpcError {
676            code,
677            message: message.to_string(),
678            data,
679        }),
680    }
681}
682
683// ---------- Method params ----------
684
685/// Parameters for message/send operation
686#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
687#[serde(rename_all = "camelCase")]
688pub struct SendMessageRequest {
689    /// Optional tenant for multi-tenancy
690    #[serde(default, skip_serializing_if = "Option::is_none")]
691    pub tenant: Option<String>,
692    /// The message to send
693    pub message: Message,
694    /// Optional request configuration
695    #[serde(default, skip_serializing_if = "Option::is_none")]
696    pub configuration: Option<SendMessageConfiguration>,
697    /// Custom metadata
698    #[serde(default, skip_serializing_if = "Option::is_none")]
699    pub metadata: Option<serde_json::Value>,
700}
701
702/// Configuration for message send requests
703#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
704#[serde(rename_all = "camelCase")]
705pub struct SendMessageConfiguration {
706    /// Preferred output MIME types
707    #[serde(default, skip_serializing_if = "Option::is_none")]
708    pub accepted_output_modes: Option<Vec<String>>,
709    /// Push notification configuration for this request
710    #[serde(default, skip_serializing_if = "Option::is_none")]
711    pub push_notification_config: Option<PushNotificationConfig>,
712    /// Message history depth (0 = omit, None = server default)
713    #[serde(default, skip_serializing_if = "Option::is_none")]
714    pub history_length: Option<u32>,
715    /// Wait for task completion (default: false)
716    #[serde(default, skip_serializing_if = "Option::is_none")]
717    pub blocking: Option<bool>,
718    /// Return immediately without waiting for completion (default: false).
719    /// When true, the server returns the task in its current state even if non-terminal.
720    #[serde(default, skip_serializing_if = "Option::is_none")]
721    pub return_immediately: Option<bool>,
722}
723
724/// Parameters for tasks/get operation
725#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
726#[serde(rename_all = "camelCase")]
727pub struct GetTaskRequest {
728    /// Task ID
729    pub id: String,
730    /// Message history depth
731    #[serde(default, skip_serializing_if = "Option::is_none")]
732    pub history_length: Option<u32>,
733    /// Optional tenant
734    #[serde(default, skip_serializing_if = "Option::is_none")]
735    pub tenant: Option<String>,
736}
737
738/// Parameters for tasks/cancel operation
739#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
740#[serde(rename_all = "camelCase")]
741pub struct CancelTaskRequest {
742    /// Task ID
743    pub id: String,
744    /// Optional tenant
745    #[serde(default, skip_serializing_if = "Option::is_none")]
746    pub tenant: Option<String>,
747}
748
749/// Parameters for tasks/list operation
750#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
751#[serde(rename_all = "camelCase")]
752pub struct ListTasksRequest {
753    /// Filter by context ID
754    #[serde(default, skip_serializing_if = "Option::is_none")]
755    pub context_id: Option<String>,
756    /// Filter by task state
757    #[serde(default, skip_serializing_if = "Option::is_none")]
758    pub status: Option<TaskState>,
759    /// Results per page (1-100, default 50)
760    #[serde(default, skip_serializing_if = "Option::is_none")]
761    pub page_size: Option<u32>,
762    /// Pagination cursor
763    #[serde(default, skip_serializing_if = "Option::is_none")]
764    pub page_token: Option<String>,
765    /// History depth per task
766    #[serde(default, skip_serializing_if = "Option::is_none")]
767    pub history_length: Option<u32>,
768    /// Filter by status timestamp after (ISO 8601 or millis)
769    #[serde(default, skip_serializing_if = "Option::is_none")]
770    pub status_timestamp_after: Option<i64>,
771    /// Include artifacts in response
772    #[serde(default, skip_serializing_if = "Option::is_none")]
773    pub include_artifacts: Option<bool>,
774    /// Optional tenant
775    #[serde(default, skip_serializing_if = "Option::is_none")]
776    pub tenant: Option<String>,
777}
778
779/// Response for tasks/list operation
780#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
781#[serde(rename_all = "camelCase")]
782pub struct TaskListResponse {
783    /// Tasks matching the query
784    pub tasks: Vec<Task>,
785    /// Empty string if this is the final page
786    pub next_page_token: String,
787    /// Requested page size
788    pub page_size: u32,
789    /// Total matching tasks
790    pub total_size: u32,
791}
792
793/// Parameters for tasks/subscribe operation
794#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
795#[serde(rename_all = "camelCase")]
796pub struct SubscribeToTaskRequest {
797    /// Task ID
798    pub id: String,
799    /// Optional tenant
800    #[serde(default, skip_serializing_if = "Option::is_none")]
801    pub tenant: Option<String>,
802}
803
804/// Push notification configuration per proto spec
805#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
806#[serde(rename_all = "camelCase")]
807pub struct PushNotificationConfig {
808    /// Configuration identifier
809    #[serde(default, skip_serializing_if = "Option::is_none")]
810    pub id: Option<String>,
811    /// Webhook URL to receive notifications
812    pub url: String,
813    /// Token for webhook authentication
814    #[serde(default, skip_serializing_if = "Option::is_none")]
815    pub token: Option<String>,
816    /// Authentication details for webhook delivery
817    #[serde(default, skip_serializing_if = "Option::is_none")]
818    pub authentication: Option<AuthenticationInfo>,
819}
820
821/// Authentication info for push notification delivery per proto spec
822#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
823#[serde(rename_all = "camelCase")]
824pub struct AuthenticationInfo {
825    /// Auth scheme (e.g., "bearer", "api_key")
826    pub scheme: String,
827    /// Credentials (e.g., token value) — optional in RC 1.0
828    #[serde(default, skip_serializing_if = "Option::is_none")]
829    pub credentials: Option<String>,
830}
831
832/// Wrapper for push notification config with task context
833#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
834#[serde(rename_all = "camelCase")]
835pub struct TaskPushNotificationConfig {
836    /// Configuration ID
837    pub id: String,
838    /// Associated task ID
839    pub task_id: String,
840    /// The push notification configuration
841    pub push_notification_config: PushNotificationConfig,
842    /// Optional tenant
843    #[serde(default, skip_serializing_if = "Option::is_none")]
844    pub tenant: Option<String>,
845}
846
847/// Parameters for tasks/pushNotificationConfig/create
848#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
849#[serde(rename_all = "camelCase")]
850pub struct CreateTaskPushNotificationConfigRequest {
851    /// Task ID
852    pub task_id: String,
853    /// Configuration identifier
854    pub config_id: String,
855    /// Configuration details
856    pub push_notification_config: PushNotificationConfig,
857    /// Optional tenant
858    #[serde(default, skip_serializing_if = "Option::is_none")]
859    pub tenant: Option<String>,
860}
861
862/// Parameters for tasks/pushNotificationConfig/get
863#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
864#[serde(rename_all = "camelCase")]
865pub struct GetTaskPushNotificationConfigRequest {
866    /// Config ID
867    pub id: String,
868    /// Task ID
869    pub task_id: String,
870    /// Optional tenant
871    #[serde(default, skip_serializing_if = "Option::is_none")]
872    pub tenant: Option<String>,
873}
874
875/// Parameters for tasks/pushNotificationConfig/list
876#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
877#[serde(rename_all = "camelCase")]
878pub struct ListTaskPushNotificationConfigRequest {
879    /// Task ID
880    pub task_id: String,
881    /// Max configs to return
882    #[serde(default, skip_serializing_if = "Option::is_none")]
883    pub page_size: Option<u32>,
884    /// Pagination cursor
885    #[serde(default, skip_serializing_if = "Option::is_none")]
886    pub page_token: Option<String>,
887    /// Optional tenant
888    #[serde(default, skip_serializing_if = "Option::is_none")]
889    pub tenant: Option<String>,
890}
891
892/// Response for tasks/pushNotificationConfig/list
893#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
894#[serde(rename_all = "camelCase")]
895pub struct ListTaskPushNotificationConfigResponse {
896    /// Push notification configurations
897    pub configs: Vec<TaskPushNotificationConfig>,
898    /// Next page token
899    #[serde(default)]
900    pub next_page_token: String,
901}
902
903/// Parameters for tasks/pushNotificationConfig/delete
904#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
905#[serde(rename_all = "camelCase")]
906pub struct DeleteTaskPushNotificationConfigRequest {
907    /// Config ID
908    pub id: String,
909    /// Task ID
910    pub task_id: String,
911    /// Optional tenant
912    #[serde(default, skip_serializing_if = "Option::is_none")]
913    pub tenant: Option<String>,
914}
915
916/// Parameters for agentCard/getExtended
917#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
918#[serde(rename_all = "camelCase")]
919pub struct GetExtendedAgentCardRequest {
920    /// Optional tenant
921    #[serde(default, skip_serializing_if = "Option::is_none")]
922    pub tenant: Option<String>,
923}
924
925// ---------- Streaming event types ----------
926
927/// Streaming response per proto spec — uses externally tagged oneof
928#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
929#[serde(rename_all = "camelCase")]
930pub enum StreamResponse {
931    /// Complete task snapshot
932    Task(Task),
933    /// Direct message response
934    Message(Message),
935    /// Task status update event
936    StatusUpdate(TaskStatusUpdateEvent),
937    /// Task artifact update event
938    ArtifactUpdate(TaskArtifactUpdateEvent),
939}
940
941/// Task status update event
942#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
943#[serde(rename_all = "camelCase")]
944pub struct TaskStatusUpdateEvent {
945    /// Kind discriminator — always "status-update"
946    #[serde(default = "default_status_update_kind")]
947    pub kind: String,
948    /// Task identifier
949    pub task_id: String,
950    /// Context identifier
951    pub context_id: String,
952    /// Updated status
953    pub status: TaskStatus,
954    /// Whether this is the final event in the stream
955    #[serde(rename = "final", default)]
956    pub is_final: bool,
957    /// Custom metadata
958    #[serde(default, skip_serializing_if = "Option::is_none")]
959    pub metadata: Option<serde_json::Value>,
960}
961
962/// Task artifact update event
963#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
964#[serde(rename_all = "camelCase")]
965pub struct TaskArtifactUpdateEvent {
966    /// Kind discriminator — always "artifact-update"
967    #[serde(default = "default_artifact_update_kind")]
968    pub kind: String,
969    /// Task identifier
970    pub task_id: String,
971    /// Context identifier
972    pub context_id: String,
973    /// New or updated artifact
974    pub artifact: Artifact,
975    /// Whether to append to existing artifact
976    #[serde(default, skip_serializing_if = "Option::is_none")]
977    pub append: Option<bool>,
978    /// Whether this is the last chunk
979    #[serde(default, skip_serializing_if = "Option::is_none")]
980    pub last_chunk: Option<bool>,
981    /// Custom metadata
982    #[serde(default, skip_serializing_if = "Option::is_none")]
983    pub metadata: Option<serde_json::Value>,
984}
985
986/// Wire-format result for streaming events — the value inside each SSE JSON-RPC `result` field.
987///
988/// Uses untagged serialization; each variant is distinguished by its `kind` field and
989/// unique required fields.
990#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
991#[serde(untagged)]
992pub enum StreamingMessageResult {
993    StatusUpdate(TaskStatusUpdateEvent),
994    ArtifactUpdate(TaskArtifactUpdateEvent),
995    Task(Task),
996    Message(Message),
997}
998
999// ---------- Helper functions ----------
1000
1001/// Create a new message with text content
1002pub fn new_message(role: Role, text: &str, context_id: Option<String>) -> Message {
1003    Message {
1004        kind: "message".to_string(),
1005        message_id: Uuid::new_v4().to_string(),
1006        context_id,
1007        task_id: None,
1008        role,
1009        parts: vec![Part::text(text)],
1010        metadata: None,
1011        extensions: vec![],
1012        reference_task_ids: None,
1013    }
1014}
1015
1016/// Create a completed task with text response
1017pub fn completed_task_with_text(user_message: Message, reply_text: &str) -> Task {
1018    let context_id = user_message
1019        .context_id
1020        .clone()
1021        .unwrap_or_else(|| Uuid::new_v4().to_string());
1022    let task_id = Uuid::new_v4().to_string();
1023    let agent_msg = new_message(Role::Agent, reply_text, Some(context_id.clone()));
1024
1025    Task {
1026        kind: "task".to_string(),
1027        id: task_id,
1028        context_id,
1029        status: TaskStatus {
1030            state: TaskState::Completed,
1031            message: Some(agent_msg.clone()),
1032            timestamp: Some(chrono::Utc::now().to_rfc3339()),
1033        },
1034        history: Some(vec![user_message, agent_msg]),
1035        artifacts: None,
1036        metadata: None,
1037    }
1038}
1039
1040/// Generate ISO 8601 timestamp
1041pub fn now_iso8601() -> String {
1042    chrono::Utc::now().to_rfc3339()
1043}
1044
1045/// Validate task ID format (UUID)
1046pub fn validate_task_id(id: &str) -> bool {
1047    Uuid::parse_str(id).is_ok()
1048}
1049
1050#[cfg(test)]
1051mod tests {
1052    use super::*;
1053
1054    #[test]
1055    fn jsonrpc_helpers_round_trip() {
1056        let resp = success(serde_json::json!(1), serde_json::json!({"ok": true}));
1057        assert_eq!(resp.jsonrpc, "2.0");
1058        assert!(resp.error.is_none());
1059        assert!(resp.result.is_some());
1060    }
1061
1062    #[test]
1063    fn task_state_is_terminal() {
1064        assert!(TaskState::Completed.is_terminal());
1065        assert!(TaskState::Failed.is_terminal());
1066        assert!(TaskState::Canceled.is_terminal());
1067        assert!(TaskState::Rejected.is_terminal());
1068        assert!(!TaskState::Working.is_terminal());
1069        assert!(!TaskState::Submitted.is_terminal());
1070        assert!(!TaskState::InputRequired.is_terminal());
1071    }
1072
1073    #[test]
1074    fn task_state_serialization() {
1075        let state = TaskState::Working;
1076        let json = serde_json::to_string(&state).unwrap();
1077        assert_eq!(json, r#""working""#);
1078
1079        let parsed: TaskState = serde_json::from_str(&json).unwrap();
1080        assert_eq!(parsed, TaskState::Working);
1081    }
1082
1083    #[test]
1084    fn role_serialization() {
1085        let role = Role::User;
1086        let json = serde_json::to_string(&role).unwrap();
1087        assert_eq!(json, r#""user""#);
1088    }
1089
1090    #[test]
1091    fn message_serialization() {
1092        let msg = new_message(Role::User, "hello", Some("ctx-123".to_string()));
1093        let json = serde_json::to_string(&msg).unwrap();
1094        let parsed: Message = serde_json::from_str(&json).unwrap();
1095        assert_eq!(parsed.role, Role::User);
1096        assert_eq!(parsed.parts.len(), 1);
1097        assert_eq!(parsed.parts[0].as_text(), Some("hello"));
1098
1099        // Verify camelCase field names
1100        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1101        assert!(value.get("messageId").is_some());
1102        assert!(value.get("contextId").is_some());
1103    }
1104
1105    #[test]
1106    fn task_serialization() {
1107        let user_msg = new_message(Role::User, "test", None);
1108        let task = completed_task_with_text(user_msg, "response");
1109        let json = serde_json::to_string(&task).unwrap();
1110        let parsed: Task = serde_json::from_str(&json).unwrap();
1111        assert_eq!(parsed.status.state, TaskState::Completed);
1112        assert!(parsed.history.is_some());
1113        assert_eq!(parsed.history.unwrap().len(), 2);
1114
1115        // Verify camelCase
1116        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1117        assert!(value.get("contextId").is_some());
1118    }
1119
1120    #[test]
1121    fn part_text_serialization() {
1122        let part = Part::text("hello");
1123        let json = serde_json::to_string(&part).unwrap();
1124        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1125        assert_eq!(value.get("kind").unwrap().as_str().unwrap(), "text");
1126        assert_eq!(value.get("text").unwrap().as_str().unwrap(), "hello");
1127    }
1128
1129    #[test]
1130    fn part_text_round_trip() {
1131        let part = Part::text("hello");
1132        let json = serde_json::to_string(&part).unwrap();
1133        let parsed: Part = serde_json::from_str(&json).unwrap();
1134        assert_eq!(parsed, part);
1135        assert_eq!(parsed.as_text(), Some("hello"));
1136    }
1137
1138    #[test]
1139    fn part_file_uri_serialization() {
1140        let part = Part::file_uri("https://example.com/file.pdf", "application/pdf");
1141        let json = serde_json::to_string(&part).unwrap();
1142        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1143        assert_eq!(value.get("kind").unwrap().as_str().unwrap(), "file");
1144        let file = value.get("file").unwrap();
1145        assert_eq!(
1146            file.get("uri").unwrap().as_str().unwrap(),
1147            "https://example.com/file.pdf"
1148        );
1149        assert_eq!(
1150            file.get("mimeType").unwrap().as_str().unwrap(),
1151            "application/pdf"
1152        );
1153    }
1154
1155    #[test]
1156    fn part_data_serialization() {
1157        let part = Part::data(serde_json::json!({"key": "value"}));
1158        let json = serde_json::to_string(&part).unwrap();
1159        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1160        assert_eq!(value.get("kind").unwrap().as_str().unwrap(), "data");
1161        assert_eq!(
1162            value.get("data").unwrap(),
1163            &serde_json::json!({"key": "value"})
1164        );
1165    }
1166
1167    #[test]
1168    fn part_deserialization_from_wire_format() {
1169        // Verify we can deserialize the exact wire format other SDKs produce
1170        let text: Part = serde_json::from_str(r#"{"kind":"text","text":"hello"}"#).unwrap();
1171        assert_eq!(text.as_text(), Some("hello"));
1172
1173        let file: Part = serde_json::from_str(
1174            r#"{"kind":"file","file":{"uri":"https://example.com/f.pdf","mimeType":"application/pdf"}}"#,
1175        )
1176        .unwrap();
1177        match &file {
1178            Part::File { file, .. } => {
1179                assert_eq!(file.uri.as_deref(), Some("https://example.com/f.pdf"));
1180                assert_eq!(file.mime_type.as_deref(), Some("application/pdf"));
1181            }
1182            _ => panic!("expected File part"),
1183        }
1184
1185        let data: Part =
1186            serde_json::from_str(r#"{"kind":"data","data":{"k":"v"}}"#).unwrap();
1187        match &data {
1188            Part::Data { data, .. } => assert_eq!(data, &serde_json::json!({"k": "v"})),
1189            _ => panic!("expected Data part"),
1190        }
1191    }
1192
1193    #[test]
1194    fn agent_card_with_security() {
1195        let card = AgentCard {
1196            name: "Test Agent".to_string(),
1197            description: "Test description".to_string(),
1198            supported_interfaces: vec![AgentInterface {
1199                url: "https://example.com/v1/rpc".to_string(),
1200                protocol_binding: "JSONRPC".to_string(),
1201                protocol_version: PROTOCOL_VERSION.to_string(),
1202                tenant: None,
1203            }],
1204            provider: Some(AgentProvider {
1205                organization: "Test Org".to_string(),
1206                url: "https://example.com".to_string(),
1207            }),
1208            version: PROTOCOL_VERSION.to_string(),
1209            documentation_url: None,
1210            capabilities: AgentCapabilities::default(),
1211            security_schemes: {
1212                let mut m = HashMap::new();
1213                m.insert(
1214                    "apiKey".to_string(),
1215                    SecurityScheme::ApiKeySecurityScheme(ApiKeySecurityScheme {
1216                        name: "X-API-Key".to_string(),
1217                        location: "header".to_string(),
1218                        description: None,
1219                    }),
1220                );
1221                m
1222            },
1223            security_requirements: vec![],
1224            default_input_modes: vec![],
1225            default_output_modes: vec![],
1226            skills: vec![],
1227            signatures: vec![],
1228            icon_url: None,
1229        };
1230
1231        let json = serde_json::to_string(&card).unwrap();
1232        let parsed: AgentCard = serde_json::from_str(&json).unwrap();
1233        assert_eq!(parsed.name, "Test Agent");
1234        assert_eq!(parsed.security_schemes.len(), 1);
1235        assert_eq!(parsed.endpoint(), Some("https://example.com/v1/rpc"));
1236
1237        // Verify camelCase
1238        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1239        assert!(value.get("supportedInterfaces").is_some());
1240        assert!(value.get("securitySchemes").is_some());
1241        assert!(value.get("securityRequirements").is_some());
1242    }
1243
1244    #[test]
1245    fn validate_task_id_helper() {
1246        let valid_uuid = Uuid::new_v4().to_string();
1247        assert!(validate_task_id(&valid_uuid));
1248        assert!(!validate_task_id("not-a-uuid"));
1249    }
1250
1251    #[test]
1252    fn error_codes() {
1253        use errors::*;
1254        assert_eq!(message_for_code(TASK_NOT_FOUND), "Task not found");
1255        assert_eq!(
1256            message_for_code(VERSION_NOT_SUPPORTED),
1257            "Protocol version not supported"
1258        );
1259        assert_eq!(
1260            message_for_code(INVALID_AGENT_RESPONSE),
1261            "Invalid agent response"
1262        );
1263        assert_eq!(
1264            message_for_code(EXTENSION_SUPPORT_REQUIRED),
1265            "Extension support required"
1266        );
1267        assert_eq!(message_for_code(999), "Unknown error");
1268    }
1269
1270    #[test]
1271    fn send_message_result_serialization() {
1272        let task = Task {
1273            kind: "task".to_string(),
1274            id: "t-1".to_string(),
1275            context_id: "ctx-1".to_string(),
1276            status: TaskStatus {
1277                state: TaskState::Completed,
1278                message: None,
1279                timestamp: None,
1280            },
1281            artifacts: None,
1282            history: None,
1283            metadata: None,
1284        };
1285
1286        // SendMessageResult (wire format) serializes the Task directly — no wrapper key
1287        let result = SendMessageResult::Task(task.clone());
1288        let json = serde_json::to_string(&result).unwrap();
1289        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1290        assert!(value.get("task").is_none(), "should not have wrapper key");
1291        assert_eq!(value.get("id").unwrap().as_str().unwrap(), "t-1");
1292
1293        // Round-trip: deserialize bare task JSON as SendMessageResult
1294        let parsed: SendMessageResult = serde_json::from_str(&json).unwrap();
1295        assert_eq!(parsed, SendMessageResult::Task(task));
1296    }
1297
1298    #[test]
1299    fn stream_response_serialization() {
1300        let event = StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
1301            kind: "status-update".to_string(),
1302            task_id: "t-1".to_string(),
1303            context_id: "ctx-1".to_string(),
1304            status: TaskStatus {
1305                state: TaskState::Working,
1306                message: None,
1307                timestamp: None,
1308            },
1309            is_final: false,
1310            metadata: None,
1311        });
1312        let json = serde_json::to_string(&event).unwrap();
1313        let value: serde_json::Value = serde_json::from_str(&json).unwrap();
1314        assert!(value.get("statusUpdate").is_some());
1315    }
1316
1317    #[test]
1318    fn push_notification_config_serialization() {
1319        let config = PushNotificationConfig {
1320            id: Some("cfg-1".to_string()),
1321            url: "https://example.com/webhook".to_string(),
1322            token: Some("secret".to_string()),
1323            authentication: Some(AuthenticationInfo {
1324                scheme: "bearer".to_string(),
1325                credentials: Some("token123".to_string()),
1326            }),
1327        };
1328        let json = serde_json::to_string(&config).unwrap();
1329        let parsed: PushNotificationConfig = serde_json::from_str(&json).unwrap();
1330        assert_eq!(parsed.url, "https://example.com/webhook");
1331        assert_eq!(parsed.authentication.unwrap().scheme, "bearer");
1332    }
1333}