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