Skip to main content

vtcode_a2a/
rpc.rs

1//! JSON-RPC 2.0 structures for A2A Protocol
2//!
3//! Implements the JSON-RPC 2.0 request/response format used by the A2A protocol,
4//! along with A2A-specific RPC method constants.
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use super::errors::A2aErrorCode;
10
11// ============================================================================
12// A2A RPC Method Constants
13// ============================================================================
14
15/// Send a message to initiate or continue a task
16pub const METHOD_MESSAGE_SEND: &str = "message/send";
17
18/// Send a message and subscribe to real-time updates via SSE
19pub const METHOD_MESSAGE_STREAM: &str = "message/stream";
20
21/// Retrieve the current state of a task
22pub const METHOD_TASKS_GET: &str = "tasks/get";
23
24/// Retrieve a list of tasks with optional filtering
25pub const METHOD_TASKS_LIST: &str = "tasks/list";
26
27/// Request cancellation of a running task
28pub const METHOD_TASKS_CANCEL: &str = "tasks/cancel";
29
30/// Set push notification configuration for a task
31pub const METHOD_TASKS_PUSH_CONFIG_SET: &str = "tasks/pushNotificationConfig/set";
32
33/// Get push notification configuration for a task
34pub const METHOD_TASKS_PUSH_CONFIG_GET: &str = "tasks/pushNotificationConfig/get";
35
36/// Resubscribe to task updates after connection interruption
37pub const METHOD_TASKS_RESUBSCRIBE: &str = "tasks/resubscribe";
38
39/// Get authenticated extended agent card
40pub const METHOD_AGENT_GET_EXTENDED_CARD: &str = "agent/getAuthenticatedExtendedCard";
41
42// ============================================================================
43// JSON-RPC 2.0 Structures
44// ============================================================================
45
46/// JSON-RPC 2.0 version constant
47pub const JSONRPC_VERSION: &str = "2.0";
48
49/// JSON-RPC 2.0 Request
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct JsonRpcRequest {
52    /// Protocol version (always "2.0")
53    pub jsonrpc: String,
54    /// Method name
55    pub method: String,
56    /// Method parameters
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub params: Option<Value>,
59    /// Request ID (can be string, number, or null)
60    pub id: Value,
61}
62
63impl JsonRpcRequest {
64    /// Create a new JSON-RPC request
65    pub fn new(method: impl Into<String>, params: Option<Value>, id: Value) -> Self {
66        Self {
67            jsonrpc: JSONRPC_VERSION.to_string(),
68            method: method.into(),
69            params,
70            id,
71        }
72    }
73
74    /// Create a request with a string ID
75    pub fn with_string_id(method: impl Into<String>, params: Option<Value>, id: impl Into<String>) -> Self {
76        Self::new(method, params, Value::String(id.into()))
77    }
78
79    /// Create a request with a numeric ID
80    pub fn with_numeric_id(method: impl Into<String>, params: Option<Value>, id: i64) -> Self {
81        Self::new(method, params, Value::Number(id.into()))
82    }
83
84    /// Create a message/send request
85    pub fn message_send(params: MessageSendParams, id: Value) -> Self {
86        Self::new(METHOD_MESSAGE_SEND, Some(serde_json::to_value(params).unwrap_or_default()), id)
87    }
88
89    /// Create a tasks/get request
90    pub fn tasks_get(task_id: impl Into<String>, id: Value) -> Self {
91        Self::new(METHOD_TASKS_GET, Some(serde_json::json!({ "id": task_id.into() })), id)
92    }
93}
94
95/// JSON-RPC 2.0 Response
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct JsonRpcResponse {
98    /// Protocol version (always "2.0")
99    pub jsonrpc: String,
100    /// Result (present on success)
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub result: Option<Value>,
103    /// Error (present on failure)
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub error: Option<JsonRpcError>,
106    /// Request ID (matches the request)
107    pub id: Value,
108}
109
110impl JsonRpcResponse {
111    /// Create a success response
112    pub fn success(result: Value, id: Value) -> Self {
113        Self {
114            jsonrpc: JSONRPC_VERSION.to_string(),
115            result: Some(result),
116            error: None,
117            id,
118        }
119    }
120
121    /// Create an error response
122    pub fn error(error: JsonRpcError, id: Value) -> Self {
123        Self {
124            jsonrpc: JSONRPC_VERSION.to_string(),
125            result: None,
126            error: Some(error),
127            id,
128        }
129    }
130
131    /// Check if this is a success response
132    pub fn is_success(&self) -> bool {
133        self.result.is_some() && self.error.is_none()
134    }
135
136    /// Check if this is an error response
137    pub fn is_error(&self) -> bool {
138        self.error.is_some()
139    }
140}
141
142/// JSON-RPC 2.0 Error
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct JsonRpcError {
145    /// Error code
146    pub code: i32,
147    /// Error message
148    pub message: String,
149    /// Additional error data
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub data: Option<Value>,
152}
153
154impl JsonRpcError {
155    /// Create a new error
156    pub fn new(code: i32, message: impl Into<String>) -> Self {
157        Self { code, message: message.into(), data: None }
158    }
159
160    /// Create an error with additional data
161    pub fn with_data(code: i32, message: impl Into<String>, data: Value) -> Self {
162        Self { code, message: message.into(), data: Some(data) }
163    }
164
165    /// Create an error from A2aErrorCode
166    pub fn from_code(code: A2aErrorCode, message: impl Into<String>) -> Self {
167        Self::new(code.into(), message)
168    }
169
170    /// Create a parse error
171    pub fn parse_error(message: impl Into<String>) -> Self {
172        Self::from_code(A2aErrorCode::JsonParseError, message)
173    }
174
175    /// Create an invalid request error
176    pub fn invalid_request(message: impl Into<String>) -> Self {
177        Self::from_code(A2aErrorCode::InvalidRequest, message)
178    }
179
180    /// Create a method not found error
181    pub fn method_not_found(method: impl Into<String>) -> Self {
182        Self::from_code(A2aErrorCode::MethodNotFound, format!("Method not found: {}", method.into()))
183    }
184
185    /// Create an invalid params error
186    pub fn invalid_params(message: impl Into<String>) -> Self {
187        Self::from_code(A2aErrorCode::InvalidParams, message)
188    }
189
190    /// Create an internal error
191    pub fn internal_error(message: impl Into<String>) -> Self {
192        Self::from_code(A2aErrorCode::InternalError, message)
193    }
194
195    /// Create a task not found error
196    pub fn task_not_found(task_id: impl Into<String>) -> Self {
197        Self::from_code(A2aErrorCode::TaskNotFound, format!("Task not found: {}", task_id.into()))
198    }
199}
200
201// ============================================================================
202// Request Parameters
203// ============================================================================
204
205/// Parameters for message/send and message/stream methods
206#[derive(Debug, Clone, Serialize, Deserialize)]
207#[serde(rename_all = "camelCase")]
208pub struct MessageSendParams {
209    /// Message to send
210    pub message: super::types::Message,
211    /// Optional task ID (to continue existing task)
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub task_id: Option<String>,
214    /// Optional context ID (for conversational context)
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub context_id: Option<String>,
217    /// Optional configuration
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub configuration: Option<MessageConfiguration>,
220}
221
222impl MessageSendParams {
223    /// Create new message send params
224    pub fn new(message: super::types::Message) -> Self {
225        Self {
226            message,
227            task_id: None,
228            context_id: None,
229            configuration: None,
230        }
231    }
232
233    /// Set the task ID
234    pub fn with_task_id(mut self, task_id: impl Into<String>) -> Self {
235        self.task_id = Some(task_id.into());
236        self
237    }
238
239    /// Set the context ID
240    pub fn with_context_id(mut self, context_id: impl Into<String>) -> Self {
241        self.context_id = Some(context_id.into());
242        self
243    }
244}
245
246/// Configuration for message sending
247#[derive(Debug, Clone, Serialize, Deserialize)]
248#[serde(rename_all = "camelCase")]
249pub struct MessageConfiguration {
250    /// Accepted input MIME types
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub accepted_input_modes: Option<Vec<String>>,
253    /// Accepted output MIME types
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub accepted_output_modes: Option<Vec<String>>,
256    /// History length to include
257    #[serde(skip_serializing_if = "Option::is_none")]
258    pub history_length: Option<u32>,
259    /// Push notification configuration
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub push_notification_config: Option<PushNotificationConfig>,
262}
263
264/// Push notification configuration
265#[derive(Debug, Clone, Serialize, Deserialize)]
266#[serde(rename_all = "camelCase")]
267pub struct PushNotificationConfig {
268    /// Webhook URL to receive notifications
269    pub url: String,
270    /// Optional authentication header value
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub authentication: Option<String>,
273}
274
275/// Parameters for tasks/get method
276#[derive(Debug, Clone, Serialize, Deserialize)]
277#[serde(rename_all = "camelCase")]
278pub struct TaskQueryParams {
279    /// Task ID
280    pub id: String,
281    /// Optional history length
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub history_length: Option<u32>,
284}
285
286/// Parameters for tasks/list method
287#[derive(Debug, Clone, Serialize, Deserialize, Default)]
288#[serde(rename_all = "camelCase")]
289pub struct ListTasksParams {
290    /// Filter by context ID
291    #[serde(skip_serializing_if = "Option::is_none")]
292    pub context_id: Option<String>,
293    /// Filter by status
294    #[serde(skip_serializing_if = "Option::is_none")]
295    pub status: Option<super::types::TaskState>,
296    /// Page size
297    #[serde(skip_serializing_if = "Option::is_none")]
298    pub page_size: Option<u32>,
299    /// Page token for pagination
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub page_token: Option<String>,
302    /// History length to include
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub history_length: Option<u32>,
305    /// Filter by last updated timestamp
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub last_updated_after: Option<String>,
308    /// Include artifacts in response
309    #[serde(skip_serializing_if = "Option::is_none")]
310    pub include_artifacts: Option<bool>,
311    /// Filter by metadata
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub metadata: Option<Value>,
314}
315
316/// Result for tasks/list method
317#[derive(Debug, Clone, Serialize, Deserialize)]
318#[serde(rename_all = "camelCase")]
319pub struct ListTasksResult {
320    /// List of tasks
321    pub tasks: Vec<super::types::Task>,
322    /// Total number of tasks matching the query
323    #[serde(skip_serializing_if = "Option::is_none")]
324    pub total_size: Option<u32>,
325    /// Page size used
326    #[serde(skip_serializing_if = "Option::is_none")]
327    pub page_size: Option<u32>,
328    /// Token for next page
329    #[serde(skip_serializing_if = "Option::is_none")]
330    pub next_page_token: Option<String>,
331}
332
333/// Parameters for tasks/cancel method
334#[derive(Debug, Clone, Serialize, Deserialize)]
335#[serde(rename_all = "camelCase")]
336pub struct TaskIdParams {
337    /// Task ID
338    pub id: String,
339}
340
341/// Configuration for push notification delivery
342#[derive(Debug, Clone, Serialize, Deserialize)]
343#[serde(rename_all = "camelCase")]
344pub struct TaskPushNotificationConfig {
345    /// Task ID to configure
346    pub task_id: String,
347    /// Webhook URL for notifications
348    pub url: String,
349    /// Optional authentication header value (Bearer token, API key, etc.)
350    #[serde(skip_serializing_if = "Option::is_none")]
351    pub authentication: Option<String>,
352}
353
354// ============================================================================
355// Streaming Events
356// ============================================================================
357
358// ============================================================================
359// Streaming Events (per A2A Specification)
360// ============================================================================
361
362/// Base wrapper for streaming message response
363#[derive(Debug, Clone, Serialize, Deserialize)]
364#[serde(rename_all = "camelCase")]
365pub struct SendStreamingMessageResponse {
366    /// Event data (one of MessageEvent, TaskStatusUpdateEvent, TaskArtifactUpdateEvent)
367    #[serde(flatten)]
368    pub event: StreamingEvent,
369}
370
371/// Streaming event types (discriminated by 'type' field)
372#[derive(Debug, Clone, Serialize, Deserialize)]
373#[serde(tag = "type", rename_all = "kebab-case")]
374pub enum StreamingEvent {
375    /// Message event from agent
376    #[serde(rename = "message")]
377    Message {
378        /// The message content
379        message: super::types::Message,
380        /// Context identifier the message is associated with
381        #[serde(skip_serializing_if = "Option::is_none")]
382        context_id: Option<String>,
383        /// Type discriminator
384        #[serde(default = "default_message_kind")]
385        kind: String,
386        /// True if this is the final message for the task
387        #[serde(default)]
388        r#final: bool,
389    },
390    /// Task status update event
391    #[serde(rename = "task-status")]
392    TaskStatus {
393        /// Task identifier
394        task_id: String,
395        /// Context identifier the task is associated with
396        #[serde(skip_serializing_if = "Option::is_none")]
397        context_id: Option<String>,
398        /// The new status
399        status: super::types::TaskStatus,
400        /// Type discriminator
401        #[serde(default = "default_status_kind")]
402        kind: String,
403        /// True if this is the terminal update for the task
404        #[serde(default)]
405        r#final: bool,
406    },
407    /// Task artifact update event
408    #[serde(rename = "task-artifact")]
409    TaskArtifact {
410        /// Task identifier
411        task_id: String,
412        /// The artifact data
413        artifact: super::types::Artifact,
414        /// If true, append parts to existing artifact; if false, replace
415        #[serde(default)]
416        append: bool,
417        /// If true, indicates this is the final update for the artifact
418        #[serde(default)]
419        last_chunk: bool,
420        /// Usually false for artifacts; can signal end concurrently with status
421        #[serde(default)]
422        r#final: bool,
423    },
424    /// Catch-all for unknown streaming event types added by the A2A spec.
425    #[serde(other)]
426    Unknown,
427}
428
429fn default_message_kind() -> String {
430    "streaming-response".to_string()
431}
432
433fn default_status_kind() -> String {
434    "status-update".to_string()
435}
436
437impl StreamingEvent {
438    /// Check if this is a final event
439    pub fn is_final(&self) -> bool {
440        match self {
441            StreamingEvent::Message { r#final, .. } => *r#final,
442            StreamingEvent::TaskStatus { r#final, .. } => *r#final,
443            StreamingEvent::TaskArtifact { r#final, .. } => *r#final,
444            StreamingEvent::Unknown => false,
445        }
446    }
447
448    /// Get the task ID if present
449    pub fn task_id(&self) -> Option<&str> {
450        match self {
451            StreamingEvent::Message { .. } => None,
452            StreamingEvent::TaskStatus { task_id, .. } => Some(task_id),
453            StreamingEvent::TaskArtifact { task_id, .. } => Some(task_id),
454            StreamingEvent::Unknown => None,
455        }
456    }
457
458    /// Get the context ID if present
459    pub fn context_id(&self) -> Option<&str> {
460        match self {
461            StreamingEvent::Message { context_id, .. } => context_id.as_deref(),
462            StreamingEvent::TaskStatus { context_id, .. } => context_id.as_deref(),
463            StreamingEvent::TaskArtifact { .. } | StreamingEvent::Unknown => None,
464        }
465    }
466}
467
468#[cfg(test)]
469mod tests {
470    use super::*;
471
472    #[test]
473    fn test_json_rpc_request_creation() {
474        let request =
475            JsonRpcRequest::with_string_id(METHOD_MESSAGE_SEND, Some(serde_json::json!({"message": {}})), "req-1");
476        assert_eq!(request.jsonrpc, "2.0");
477        assert_eq!(request.method, "message/send");
478        assert_eq!(request.id, Value::String("req-1".to_string()));
479    }
480
481    #[test]
482    fn test_json_rpc_response_success() {
483        let response =
484            JsonRpcResponse::success(serde_json::json!({"status": "ok"}), Value::String("req-1".to_string()));
485        assert!(response.is_success());
486        assert!(!response.is_error());
487    }
488
489    #[test]
490    fn test_json_rpc_response_error() {
491        let error = JsonRpcError::task_not_found("task-123");
492        let response = JsonRpcResponse::error(error, Value::String("req-1".to_string()));
493        assert!(!response.is_success());
494        assert!(response.is_error());
495    }
496
497    #[test]
498    fn test_error_code_serialization() {
499        let error = JsonRpcError::from_code(A2aErrorCode::TaskNotFound, "Task not found");
500        assert_eq!(error.code, -32001);
501    }
502
503    #[test]
504    fn test_streaming_event_message() {
505        let event = StreamingEvent::Message {
506            message: super::super::types::Message::agent_text("Response"),
507            context_id: Some("ctx-1".to_string()),
508            kind: "streaming-response".to_string(),
509            r#final: false,
510        };
511        assert!(!event.is_final());
512        assert_eq!(event.context_id(), Some("ctx-1"));
513    }
514
515    #[test]
516    fn test_streaming_event_task_status() {
517        let event = StreamingEvent::TaskStatus {
518            task_id: "task-1".to_string(),
519            context_id: None,
520            status: super::super::types::TaskStatus::new(super::super::types::TaskState::Completed),
521            kind: "status-update".to_string(),
522            r#final: true,
523        };
524        assert!(event.is_final());
525        assert_eq!(event.task_id(), Some("task-1"));
526    }
527
528    #[test]
529    fn test_streaming_event_artifact() {
530        let artifact = super::super::types::Artifact::text("art-1", "Output");
531        let event = StreamingEvent::TaskArtifact {
532            task_id: "task-1".to_string(),
533            artifact,
534            append: false,
535            last_chunk: true,
536            r#final: false,
537        };
538        assert!(!event.is_final());
539        assert_eq!(event.task_id(), Some("task-1"));
540    }
541
542    #[test]
543    fn test_send_streaming_message_response_serialization() {
544        let msg = super::super::types::Message::agent_text("Hello");
545        let response = SendStreamingMessageResponse {
546            event: StreamingEvent::Message {
547                message: msg,
548                context_id: Some("ctx-1".to_string()),
549                kind: "streaming-response".to_string(),
550                r#final: false,
551            },
552        };
553
554        let json = serde_json::to_string(&response).expect("serialize");
555        assert!(json.contains("streaming-response"));
556        assert!(json.contains("message"));
557
558        let deserialized: SendStreamingMessageResponse = serde_json::from_str(&json).expect("deserialize");
559        match deserialized.event {
560            StreamingEvent::Message { ref kind, .. } => {
561                assert_eq!(kind, "streaming-response");
562            }
563            _ => panic!("Expected Message event"),
564        }
565    }
566
567    #[test]
568    fn test_task_push_notification_config() {
569        let config = TaskPushNotificationConfig {
570            task_id: "task-1".to_string(),
571            url: "https://example.com/webhook".to_string(),
572            authentication: Some("Bearer token123".to_string()),
573        };
574
575        let json = serde_json::to_string(&config).expect("serialize");
576        let deserialized: TaskPushNotificationConfig = serde_json::from_str(&json).expect("deserialize");
577
578        assert_eq!(deserialized.task_id, "task-1");
579        assert_eq!(deserialized.url, "https://example.com/webhook");
580        assert!(deserialized.authentication.is_some());
581    }
582}