Skip to main content

zeph_a2a/
jsonrpc.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! JSON-RPC 2.0 envelope types and A2A method constants.
5//!
6//! The A2A protocol is built on JSON-RPC 2.0. Every request is a [`JsonRpcRequest`] and
7//! every response is a [`JsonRpcResponse`]. Use [`JsonRpcRequest::new`] to construct
8//! outgoing requests with a fresh UUID `id`.
9
10use serde::{Deserialize, Serialize, de::DeserializeOwned};
11
12use crate::types::Message;
13
14/// A2A method name for sending a non-streaming message to an agent.
15pub const METHOD_SEND_MESSAGE: &str = "message/send";
16/// A2A method name for sending a message and receiving an SSE stream of events.
17pub const METHOD_SEND_STREAMING_MESSAGE: &str = "message/stream";
18/// A2A method name for retrieving a task by ID.
19pub const METHOD_GET_TASK: &str = "tasks/get";
20/// A2A method name for canceling a task that is not yet in a terminal state.
21pub const METHOD_CANCEL_TASK: &str = "tasks/cancel";
22
23/// JSON-RPC error code indicating that the requested task ID does not exist.
24pub const ERR_TASK_NOT_FOUND: i32 = -32001;
25/// JSON-RPC error code indicating that the task is in a terminal state and cannot be canceled.
26pub const ERR_TASK_NOT_CANCELABLE: i32 = -32002;
27
28/// A JSON-RPC 2.0 request envelope carrying typed parameters `P`.
29///
30/// The `id` is always a UUID v4 string, generated in [`JsonRpcRequest::new`].
31///
32/// # Examples
33///
34/// ```rust
35/// use zeph_a2a::jsonrpc::{JsonRpcRequest, METHOD_GET_TASK, TaskIdParams};
36///
37/// let req = JsonRpcRequest::new(METHOD_GET_TASK, TaskIdParams {
38///     id: "task-123".into(),
39///     history_length: Some(10),
40/// });
41/// assert_eq!(req.method, "tasks/get");
42/// assert_eq!(req.jsonrpc, "2.0");
43/// ```
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct JsonRpcRequest<P> {
46    /// Always `"2.0"`.
47    pub jsonrpc: String,
48    /// Unique request identifier echoed back in the response.
49    pub id: serde_json::Value,
50    /// The A2A method being invoked (one of the `METHOD_*` constants).
51    pub method: String,
52    /// Method-specific parameters.
53    pub params: P,
54}
55
56/// A JSON-RPC 2.0 response envelope carrying either a typed result `R` or an error.
57///
58/// Call [`into_result`](JsonRpcResponse::into_result) to unwrap the response ergonomically.
59/// Error takes precedence if both `result` and `error` are somehow present.
60#[derive(Debug, Clone, Serialize, Deserialize)]
61#[serde(bound(deserialize = "R: Deserialize<'de>"))]
62pub struct JsonRpcResponse<R> {
63    /// Always `"2.0"`.
64    pub jsonrpc: String,
65    /// Echoed from the corresponding request `id`.
66    pub id: serde_json::Value,
67    /// Successful result payload. `None` when `error` is set.
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub result: Option<R>,
70    /// Error payload. `None` when `result` is set.
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub error: Option<JsonRpcError>,
73}
74
75/// A JSON-RPC 2.0 error object returned by the agent on method-level failures.
76///
77/// Well-known A2A codes are [`ERR_TASK_NOT_FOUND`] (`-32001`) and
78/// [`ERR_TASK_NOT_CANCELABLE`] (`-32002`).
79/// Standard JSON-RPC codes: `-32700` (parse error), `-32601` (method not found),
80/// `-32602` (invalid params), `-32603` (internal error).
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct JsonRpcError {
83    /// Numeric error code.
84    pub code: i32,
85    /// Human-readable error description.
86    pub message: String,
87    /// Optional structured detail data (not exposed to end users to avoid leaking internals).
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub data: Option<serde_json::Value>,
90}
91
92impl std::fmt::Display for JsonRpcError {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        write!(f, "JSON-RPC error {}: {}", self.code, self.message)
95    }
96}
97
98impl std::error::Error for JsonRpcError {}
99
100/// Parameters for the `message/send` and `message/stream` JSON-RPC methods.
101///
102/// # Examples
103///
104/// ```rust
105/// use zeph_a2a::jsonrpc::SendMessageParams;
106/// use zeph_a2a::Message;
107///
108/// let params = SendMessageParams {
109///     message: Message::user_text("Hello!"),
110///     configuration: None,
111/// };
112/// ```
113#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(rename_all = "camelCase")]
115pub struct SendMessageParams {
116    /// The message to process.
117    pub message: Message,
118    /// Optional per-request task configuration overrides.
119    #[serde(default, skip_serializing_if = "Option::is_none")]
120    pub configuration: Option<TaskConfiguration>,
121}
122
123/// Per-request configuration overrides for task execution.
124#[derive(Debug, Clone, Serialize, Deserialize)]
125#[serde(rename_all = "camelCase")]
126pub struct TaskConfiguration {
127    /// When `true`, the `message/send` call blocks until the task completes
128    /// and returns the full result in one response instead of returning immediately.
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub blocking: Option<bool>,
131}
132
133/// Parameters for the `tasks/get` and `tasks/cancel` JSON-RPC methods.
134#[derive(Debug, Clone, Serialize, Deserialize)]
135#[serde(rename_all = "camelCase")]
136pub struct TaskIdParams {
137    /// The task ID to look up or cancel.
138    pub id: String,
139    /// If set, limits the number of history messages returned (most recent N).
140    #[serde(default, skip_serializing_if = "Option::is_none")]
141    pub history_length: Option<u32>,
142}
143
144impl<P: Serialize> JsonRpcRequest<P> {
145    /// Create a new JSON-RPC 2.0 request with a fresh UUID `id`.
146    ///
147    /// # Examples
148    ///
149    /// ```rust
150    /// use zeph_a2a::jsonrpc::{JsonRpcRequest, METHOD_CANCEL_TASK, TaskIdParams};
151    ///
152    /// let req = JsonRpcRequest::new(METHOD_CANCEL_TASK, TaskIdParams {
153    ///     id: "task-abc".into(),
154    ///     history_length: None,
155    /// });
156    /// assert_eq!(req.method, "tasks/cancel");
157    /// ```
158    #[must_use]
159    pub fn new(method: &str, params: P) -> Self {
160        Self {
161            jsonrpc: "2.0".into(),
162            id: serde_json::Value::String(uuid::Uuid::new_v4().to_string()),
163            method: method.into(),
164            params,
165        }
166    }
167}
168
169impl<R: DeserializeOwned> JsonRpcResponse<R> {
170    /// Unwrap the response into `Ok(result)` or `Err(error)`.
171    ///
172    /// If both `error` and `result` are somehow present, the error takes precedence.
173    /// If neither is present (malformed response), returns an internal error with code `-32603`.
174    ///
175    /// # Errors
176    ///
177    /// Returns [`JsonRpcError`] if the response carries an error object, or if the response
178    /// is malformed (neither `result` nor `error`).
179    ///
180    /// # Examples
181    ///
182    /// ```rust
183    /// use zeph_a2a::jsonrpc::{JsonRpcResponse, JsonRpcError};
184    ///
185    /// let resp: JsonRpcResponse<String> = JsonRpcResponse {
186    ///     jsonrpc: "2.0".into(),
187    ///     id: serde_json::Value::String("1".into()),
188    ///     result: Some("hello".into()),
189    ///     error: None,
190    /// };
191    /// assert_eq!(resp.into_result().unwrap(), "hello");
192    /// ```
193    pub fn into_result(self) -> Result<R, JsonRpcError> {
194        if let Some(err) = self.error {
195            return Err(err);
196        }
197        self.result.ok_or_else(|| JsonRpcError {
198            code: -32603,
199            message: "response contains neither result nor error".into(),
200            data: None,
201        })
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn request_new_sets_jsonrpc_and_uuid_id() {
211        let req = JsonRpcRequest::new(
212            METHOD_SEND_MESSAGE,
213            TaskIdParams {
214                id: "task-1".into(),
215                history_length: None,
216            },
217        );
218        assert_eq!(req.jsonrpc, "2.0");
219        assert_eq!(req.method, "message/send");
220        let id_str = req.id.as_str().unwrap();
221        assert!(uuid::Uuid::parse_str(id_str).is_ok());
222    }
223
224    #[test]
225    fn request_serde_round_trip() {
226        let req = JsonRpcRequest::new(
227            METHOD_GET_TASK,
228            TaskIdParams {
229                id: "t-1".into(),
230                history_length: Some(10),
231            },
232        );
233        let json = serde_json::to_string(&req).unwrap();
234        let back: JsonRpcRequest<TaskIdParams> = serde_json::from_str(&json).unwrap();
235        assert_eq!(back.method, METHOD_GET_TASK);
236        assert_eq!(back.params.id, "t-1");
237        assert_eq!(back.params.history_length, Some(10));
238    }
239
240    #[test]
241    fn response_into_result_ok() {
242        let resp = JsonRpcResponse {
243            jsonrpc: "2.0".into(),
244            id: serde_json::Value::String("1".into()),
245            result: Some(serde_json::json!({"id": "task-1"})),
246            error: None,
247        };
248        let val: serde_json::Value = resp.into_result().unwrap();
249        assert_eq!(val["id"], "task-1");
250    }
251
252    #[test]
253    fn response_into_result_error() {
254        let resp: JsonRpcResponse<serde_json::Value> = JsonRpcResponse {
255            jsonrpc: "2.0".into(),
256            id: serde_json::Value::String("1".into()),
257            result: None,
258            error: Some(JsonRpcError {
259                code: ERR_TASK_NOT_FOUND,
260                message: "task not found".into(),
261                data: None,
262            }),
263        };
264        let err = resp.into_result().unwrap_err();
265        assert_eq!(err.code, ERR_TASK_NOT_FOUND);
266    }
267
268    #[test]
269    fn response_into_result_neither() {
270        let resp: JsonRpcResponse<serde_json::Value> = JsonRpcResponse {
271            jsonrpc: "2.0".into(),
272            id: serde_json::Value::String("1".into()),
273            result: None,
274            error: None,
275        };
276        let err = resp.into_result().unwrap_err();
277        assert_eq!(err.code, -32603);
278    }
279
280    #[test]
281    fn send_message_params_serde() {
282        let params = SendMessageParams {
283            message: Message::user_text("hello"),
284            configuration: Some(TaskConfiguration {
285                blocking: Some(true),
286            }),
287        };
288        let json = serde_json::to_string(&params).unwrap();
289        let back: SendMessageParams = serde_json::from_str(&json).unwrap();
290        assert_eq!(back.message.text_content(), Some("hello"));
291        assert_eq!(back.configuration.unwrap().blocking, Some(true));
292    }
293
294    #[test]
295    fn task_id_params_skips_none() {
296        let params = TaskIdParams {
297            id: "t-1".into(),
298            history_length: None,
299        };
300        let json = serde_json::to_string(&params).unwrap();
301        assert!(!json.contains("historyLength"));
302    }
303
304    #[test]
305    fn jsonrpc_error_display() {
306        let err = JsonRpcError {
307            code: -32001,
308            message: "not found".into(),
309            data: None,
310        };
311        assert_eq!(err.to_string(), "JSON-RPC error -32001: not found");
312    }
313}