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