vtcode_acp_client/
jsonrpc.rs

1//! JSON-RPC 2.0 types for ACP protocol compliance
2//!
3//! This module implements the JSON-RPC 2.0 specification as required by the
4//! Agent Client Protocol (ACP). All ACP methods use JSON-RPC 2.0 as the
5//! transport layer.
6//!
7//! Reference: https://agentclientprotocol.com/llms.txt
8
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12/// JSON-RPC 2.0 version string (always "2.0")
13pub const JSONRPC_VERSION: &str = "2.0";
14
15/// JSON-RPC 2.0 request object
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct JsonRpcRequest {
18    /// Protocol version (always "2.0")
19    pub jsonrpc: String,
20
21    /// Method name to invoke
22    pub method: String,
23
24    /// Method parameters (positional or named)
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub params: Option<Value>,
27
28    /// Request ID for correlation (null for notifications)
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub id: Option<JsonRpcId>,
31}
32
33/// JSON-RPC 2.0 response object
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct JsonRpcResponse {
36    /// Protocol version (always "2.0")
37    pub jsonrpc: String,
38
39    /// Result on success (mutually exclusive with error)
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub result: Option<Value>,
42
43    /// Error on failure (mutually exclusive with result)
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub error: Option<JsonRpcError>,
46
47    /// Request ID this response corresponds to
48    pub id: Option<JsonRpcId>,
49}
50
51/// JSON-RPC 2.0 error object
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct JsonRpcError {
54    /// Error code (integer)
55    pub code: i32,
56
57    /// Short error description
58    pub message: String,
59
60    /// Additional error data
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub data: Option<Value>,
63}
64
65/// JSON-RPC 2.0 request/response ID
66///
67/// Per spec, ID can be a string, number, or null
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
69#[serde(untagged)]
70pub enum JsonRpcId {
71    /// String ID
72    String(String),
73    /// Numeric ID
74    Number(i64),
75}
76
77impl JsonRpcId {
78    /// Create a new string ID
79    pub fn string(s: impl Into<String>) -> Self {
80        Self::String(s.into())
81    }
82
83    /// Create a new numeric ID
84    pub fn number(n: i64) -> Self {
85        Self::Number(n)
86    }
87
88    /// Generate a new UUID-based string ID
89    pub fn new_uuid() -> Self {
90        Self::String(uuid::Uuid::new_v4().to_string())
91    }
92}
93
94impl std::fmt::Display for JsonRpcId {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        match self {
97            JsonRpcId::String(s) => write!(f, "{}", s),
98            JsonRpcId::Number(n) => write!(f, "{}", n),
99        }
100    }
101}
102
103/// Standard JSON-RPC 2.0 error codes
104pub mod error_codes {
105    /// Parse error: Invalid JSON was received
106    pub const PARSE_ERROR: i32 = -32700;
107
108    /// Invalid request: The JSON sent is not a valid Request object
109    pub const INVALID_REQUEST: i32 = -32600;
110
111    /// Method not found: The method does not exist / is not available
112    pub const METHOD_NOT_FOUND: i32 = -32601;
113
114    /// Invalid params: Invalid method parameter(s)
115    pub const INVALID_PARAMS: i32 = -32602;
116
117    /// Internal error: Internal JSON-RPC error
118    pub const INTERNAL_ERROR: i32 = -32603;
119
120    /// Server error range: -32000 to -32099 (reserved for implementation-defined server-errors)
121    pub const SERVER_ERROR_START: i32 = -32099;
122    pub const SERVER_ERROR_END: i32 = -32000;
123
124    // ACP-specific error codes (in server error range)
125
126    /// Authentication required
127    pub const AUTH_REQUIRED: i32 = -32001;
128
129    /// Permission denied
130    pub const PERMISSION_DENIED: i32 = -32002;
131
132    /// Session not found
133    pub const SESSION_NOT_FOUND: i32 = -32003;
134
135    /// Rate limited
136    pub const RATE_LIMITED: i32 = -32004;
137
138    /// Resource not found
139    pub const RESOURCE_NOT_FOUND: i32 = -32005;
140}
141
142impl JsonRpcRequest {
143    /// Create a new JSON-RPC 2.0 request
144    pub fn new(method: impl Into<String>, params: Option<Value>, id: Option<JsonRpcId>) -> Self {
145        Self {
146            jsonrpc: JSONRPC_VERSION.to_string(),
147            method: method.into(),
148            params,
149            id,
150        }
151    }
152
153    /// Create a request with auto-generated UUID ID
154    pub fn with_uuid(method: impl Into<String>, params: Option<Value>) -> Self {
155        Self::new(method, params, Some(JsonRpcId::new_uuid()))
156    }
157
158    /// Create a notification (request without ID, no response expected)
159    pub fn notification(method: impl Into<String>, params: Option<Value>) -> Self {
160        Self::new(method, params, None)
161    }
162
163    /// Check if this is a notification (no ID means no response expected)
164    pub fn is_notification(&self) -> bool {
165        self.id.is_none()
166    }
167}
168
169impl JsonRpcResponse {
170    /// Create a successful response
171    pub fn success(result: Value, id: Option<JsonRpcId>) -> Self {
172        Self {
173            jsonrpc: JSONRPC_VERSION.to_string(),
174            result: Some(result),
175            error: None,
176            id,
177        }
178    }
179
180    /// Create an error response
181    pub fn error(error: JsonRpcError, id: Option<JsonRpcId>) -> Self {
182        Self {
183            jsonrpc: JSONRPC_VERSION.to_string(),
184            result: None,
185            error: Some(error),
186            id,
187        }
188    }
189
190    /// Check if response is successful
191    pub fn is_success(&self) -> bool {
192        self.error.is_none() && self.result.is_some()
193    }
194
195    /// Check if response is an error
196    pub fn is_error(&self) -> bool {
197        self.error.is_some()
198    }
199
200    /// Get result, returning error if response was an error
201    pub fn into_result(self) -> Result<Value, JsonRpcError> {
202        if let Some(error) = self.error {
203            Err(error)
204        } else {
205            Ok(self.result.unwrap_or(Value::Null))
206        }
207    }
208}
209
210impl JsonRpcError {
211    /// Create a new error
212    pub fn new(code: i32, message: impl Into<String>) -> Self {
213        Self {
214            code,
215            message: message.into(),
216            data: None,
217        }
218    }
219
220    /// Create error with additional data
221    pub fn with_data(code: i32, message: impl Into<String>, data: Value) -> Self {
222        Self {
223            code,
224            message: message.into(),
225            data: Some(data),
226        }
227    }
228
229    /// Create a parse error
230    pub fn parse_error(details: impl Into<String>) -> Self {
231        Self::new(error_codes::PARSE_ERROR, details)
232    }
233
234    /// Create an invalid request error
235    pub fn invalid_request(details: impl Into<String>) -> Self {
236        Self::new(error_codes::INVALID_REQUEST, details)
237    }
238
239    /// Create a method not found error
240    pub fn method_not_found(method: impl Into<String>) -> Self {
241        Self::new(
242            error_codes::METHOD_NOT_FOUND,
243            format!("Method not found: {}", method.into()),
244        )
245    }
246
247    /// Create an invalid params error
248    pub fn invalid_params(details: impl Into<String>) -> Self {
249        Self::new(error_codes::INVALID_PARAMS, details)
250    }
251
252    /// Create an internal error
253    pub fn internal_error(details: impl Into<String>) -> Self {
254        Self::new(error_codes::INTERNAL_ERROR, details)
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261    use serde_json::json;
262
263    #[test]
264    fn test_request_serialization() {
265        let req = JsonRpcRequest::new(
266            "initialize",
267            Some(json!({"protocolVersions": ["2025-01-01"]})),
268            Some(JsonRpcId::string("req-1")),
269        );
270
271        let json = serde_json::to_string(&req).unwrap();
272        assert!(json.contains("\"jsonrpc\":\"2.0\""));
273        assert!(json.contains("\"method\":\"initialize\""));
274        assert!(json.contains("\"id\":\"req-1\""));
275    }
276
277    #[test]
278    fn test_response_success() {
279        let resp = JsonRpcResponse::success(
280            json!({"session_id": "sess-123"}),
281            Some(JsonRpcId::string("req-1")),
282        );
283
284        assert!(resp.is_success());
285        assert!(!resp.is_error());
286    }
287
288    #[test]
289    fn test_response_error() {
290        let resp = JsonRpcResponse::error(
291            JsonRpcError::method_not_found("unknown"),
292            Some(JsonRpcId::string("req-1")),
293        );
294
295        assert!(resp.is_error());
296        assert!(!resp.is_success());
297    }
298
299    #[test]
300    fn test_notification() {
301        let notif = JsonRpcRequest::notification("session/update", Some(json!({"delta": "hello"})));
302
303        assert!(notif.is_notification());
304        assert!(notif.id.is_none());
305    }
306
307    #[test]
308    fn test_id_types() {
309        let string_id = JsonRpcId::string("abc");
310        let number_id = JsonRpcId::number(123);
311
312        assert_eq!(format!("{}", string_id), "abc");
313        assert_eq!(format!("{}", number_id), "123");
314    }
315}