Skip to main content

agent_client_protocol_schema/
rpc.rs

1use std::sync::Arc;
2
3use derive_more::{Display, From};
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use serde_with::skip_serializing_none;
7
8/// JSON RPC Request Id
9///
10/// An identifier established by the Client that MUST contain a String, Number, or NULL value if included. If it is not included it is assumed to be a notification. The value SHOULD normally not be Null \[1\] and Numbers SHOULD NOT contain fractional parts \[2\]
11///
12/// The Server MUST reply with the same value in the Response object if included. This member is used to correlate the context between the two objects.
13///
14/// \[1\] The use of Null as a value for the id member in a Request object is discouraged, because this specification uses a value of Null for Responses with an unknown id. Also, because JSON-RPC 1.0 uses an id value of Null for Notifications this could cause confusion in handling.
15///
16/// \[2\] Fractional parts may be problematic, since many decimal fractions cannot be represented exactly as binary fractions.
17#[derive(
18    Debug,
19    PartialEq,
20    Clone,
21    Hash,
22    Eq,
23    Deserialize,
24    Serialize,
25    PartialOrd,
26    Ord,
27    Display,
28    JsonSchema,
29    From,
30)]
31#[serde(untagged)]
32#[allow(
33    clippy::exhaustive_enums,
34    reason = "This comes from the JSON-RPC specification itself"
35)]
36#[from(String, i64)]
37pub enum RequestId {
38    #[display("null")]
39    Null,
40    Number(i64),
41    Str(String),
42}
43
44#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)]
45#[allow(
46    clippy::exhaustive_structs,
47    reason = "This comes from the JSON-RPC specification itself"
48)]
49#[schemars(rename = "{Params}", extend("x-docs-ignore" = true))]
50#[skip_serializing_none]
51pub struct Request<Params> {
52    pub id: RequestId,
53    pub method: Arc<str>,
54    pub params: Option<Params>,
55}
56
57#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)]
58#[allow(
59    clippy::exhaustive_enums,
60    reason = "This comes from the JSON-RPC specification itself"
61)]
62#[serde(untagged)]
63#[schemars(rename = "{Result}", extend("x-docs-ignore" = true))]
64pub enum Response<Result, Error> {
65    Result { id: RequestId, result: Result },
66    Error { id: RequestId, error: Error },
67}
68
69impl<R, E> Response<R, E> {
70    #[must_use]
71    pub fn new(id: impl Into<RequestId>, result: std::result::Result<R, E>) -> Self {
72        match result {
73            Ok(result) => Self::Result {
74                id: id.into(),
75                result,
76            },
77            Err(error) => Self::Error {
78                id: id.into(),
79                error,
80            },
81        }
82    }
83}
84
85#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)]
86#[allow(
87    clippy::exhaustive_structs,
88    reason = "This comes from the JSON-RPC specification itself"
89)]
90#[schemars(rename = "{Params}", extend("x-docs-ignore" = true))]
91#[skip_serializing_none]
92pub struct Notification<Params> {
93    pub method: Arc<str>,
94    pub params: Option<Params>,
95}
96
97#[derive(Debug, Serialize, Deserialize, JsonSchema)]
98#[schemars(inline)]
99enum JsonRpcVersion {
100    #[serde(rename = "2.0")]
101    V2,
102}
103
104/// A message (request, response, or notification) with `"jsonrpc": "2.0"` specified as
105/// [required by JSON-RPC 2.0 Specification][1].
106///
107/// [1]: https://www.jsonrpc.org/specification#compatibility
108#[derive(Debug, Serialize, Deserialize, JsonSchema)]
109#[schemars(inline)]
110pub struct JsonRpcMessage<M> {
111    jsonrpc: JsonRpcVersion,
112    #[serde(flatten)]
113    message: M,
114}
115
116impl<M> JsonRpcMessage<M> {
117    /// Wraps the provided message into a versioned [`JsonRpcMessage`].
118    #[must_use]
119    pub fn wrap(message: M) -> Self {
120        Self {
121            jsonrpc: JsonRpcVersion::V2,
122            message,
123        }
124    }
125
126    /// Unwraps the contained message.
127    #[must_use]
128    pub fn into_inner(self) -> M {
129        self.message
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    use crate::{
138        AgentNotification, CancelNotification, ClientNotification, ContentBlock, ContentChunk,
139        SessionId, SessionNotification, SessionUpdate, TextContent,
140    };
141    use serde_json::{Number, Value, json};
142
143    #[test]
144    fn id_deserialization() {
145        let id = serde_json::from_value::<RequestId>(Value::Null).unwrap();
146        assert_eq!(id, RequestId::Null);
147
148        let id = serde_json::from_value::<RequestId>(Value::Number(Number::from_u128(1).unwrap()))
149            .unwrap();
150        assert_eq!(id, RequestId::Number(1));
151
152        let id = serde_json::from_value::<RequestId>(Value::Number(Number::from_i128(-1).unwrap()))
153            .unwrap();
154        assert_eq!(id, RequestId::Number(-1));
155
156        let id = serde_json::from_value::<RequestId>(Value::String("id".to_owned())).unwrap();
157        assert_eq!(id, RequestId::Str("id".to_owned()));
158    }
159
160    #[test]
161    fn id_serialization() {
162        let id = serde_json::to_value(RequestId::Null).unwrap();
163        assert_eq!(id, Value::Null);
164
165        let id = serde_json::to_value(RequestId::Number(1)).unwrap();
166        assert_eq!(id, Value::Number(Number::from_u128(1).unwrap()));
167
168        let id = serde_json::to_value(RequestId::Number(-1)).unwrap();
169        assert_eq!(id, Value::Number(Number::from_i128(-1).unwrap()));
170
171        let id = serde_json::to_value(RequestId::Str("id".to_owned())).unwrap();
172        assert_eq!(id, Value::String("id".to_owned()));
173    }
174
175    #[test]
176    fn id_display() {
177        let id = RequestId::Null;
178        assert_eq!(id.to_string(), "null");
179
180        let id = RequestId::Number(1);
181        assert_eq!(id.to_string(), "1");
182
183        let id = RequestId::Number(-1);
184        assert_eq!(id.to_string(), "-1");
185
186        let id = RequestId::Str("id".to_owned());
187        assert_eq!(id.to_string(), "id");
188    }
189
190    #[test]
191    fn notification_wire_format() {
192        // Test client -> agent notification wire format
193        let outgoing_msg = JsonRpcMessage::wrap(Notification {
194            method: "cancel".into(),
195            params: Some(ClientNotification::CancelNotification(CancelNotification {
196                session_id: SessionId("test-123".into()),
197                meta: None,
198            })),
199        });
200
201        let serialized: Value = serde_json::to_value(&outgoing_msg).unwrap();
202        assert_eq!(
203            serialized,
204            json!({
205                "jsonrpc": "2.0",
206                "method": "cancel",
207                "params": {
208                    "sessionId": "test-123"
209                },
210            })
211        );
212
213        // Test agent -> client notification wire format
214        let outgoing_msg = JsonRpcMessage::wrap(Notification {
215            method: "sessionUpdate".into(),
216            params: Some(AgentNotification::SessionNotification(
217                SessionNotification {
218                    session_id: SessionId("test-456".into()),
219                    update: SessionUpdate::AgentMessageChunk(ContentChunk {
220                        content: ContentBlock::Text(TextContent {
221                            annotations: None,
222                            text: "Hello".to_string(),
223                            meta: None,
224                        }),
225                        #[cfg(feature = "unstable_message_id")]
226                        message_id: None,
227                        meta: None,
228                    }),
229                    meta: None,
230                },
231            )),
232        });
233
234        let serialized: Value = serde_json::to_value(&outgoing_msg).unwrap();
235        assert_eq!(
236            serialized,
237            json!({
238                "jsonrpc": "2.0",
239                "method": "sessionUpdate",
240                "params": {
241                    "sessionId": "test-456",
242                    "update": {
243                        "sessionUpdate": "agent_message_chunk",
244                        "content": {
245                            "type": "text",
246                            "text": "Hello"
247                        }
248                    }
249                }
250            })
251        );
252    }
253}