Skip to main content

agent_sdk_toolkit/protocol/
json_rpc.rs

1//! JSON-RPC 2.0 frame DTOs for toolkit protocol conformance. Use this module for
2//! encoded request, response, notification, and error frames. Serialization is
3//! data-only and does not own process transport.
4//!
5use agent_sdk_core::AgentError;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
10#[serde(untagged)]
11/// Identifier carried by JSON-RPC requests and responses.
12/// Use numbers or strings for ordinary correlated calls; `Null` is reserved for parse errors or
13/// protocol failures where JSON-RPC requires an id but no valid request id exists.
14pub enum JsonRpcId {
15    /// Numeric request id.
16    Number(i64),
17    /// String request id.
18    String(String),
19    /// Null id used for JSON-RPC error responses that cannot be correlated to a valid request.
20    Null,
21}
22
23impl JsonRpcId {
24    /// Returns this value as key. The accessor is side-effect free and
25    /// keeps ownership with the caller.
26    pub fn as_key(&self) -> String {
27        match self {
28            Self::Number(value) => value.to_string(),
29            Self::String(value) => value.clone(),
30            Self::Null => "null".to_string(),
31        }
32    }
33}
34
35impl From<&str> for JsonRpcId {
36    fn from(value: &str) -> Self {
37        Self::String(value.to_string())
38    }
39}
40
41impl From<String> for JsonRpcId {
42    fn from(value: String) -> Self {
43        Self::String(value)
44    }
45}
46
47impl From<i64> for JsonRpcId {
48    fn from(value: i64) -> Self {
49        Self::Number(value)
50    }
51}
52
53#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
54/// JSON-RPC request frame sent when a client expects a response.
55/// Constructing the value only prepares serialized protocol data; transport effects occur when a
56/// line endpoint sends the frame.
57pub struct JsonRpcRequest {
58    /// Protocol version marker; toolkit constructors always set this to `"2.0"`.
59    pub jsonrpc: String,
60    /// Correlation id that must be echoed by the matching response.
61    pub id: JsonRpcId,
62    /// Remote method name, such as `initialize`, `tools/list`, or a host-specific extension
63    /// method.
64    pub method: String,
65    #[serde(default)]
66    /// Method parameters serialized as JSON; absent params deserialize as an empty/default value.
67    pub params: Value,
68}
69
70impl JsonRpcRequest {
71    /// Creates a new protocol::json_rpc value with explicit
72    /// caller-provided inputs. This constructor is data-only and
73    /// performs no I/O or external side effects.
74    pub fn new(id: impl Into<JsonRpcId>, method: impl Into<String>, params: Value) -> Self {
75        Self {
76            jsonrpc: "2.0".to_string(),
77            id: id.into(),
78            method: method.into(),
79            params,
80        }
81    }
82}
83
84#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
85/// JSON-RPC notification frame sent for one-way protocol messages.
86/// Notifications have no id and do not receive responses; sending still mutates the chosen line
87/// endpoint transcript or transport.
88pub struct JsonRpcNotification {
89    /// Protocol version marker; toolkit constructors always set this to `"2.0"`.
90    pub jsonrpc: String,
91    /// Notification method name.
92    pub method: String,
93    #[serde(default)]
94    /// Notification parameters serialized as JSON; absent params deserialize as an empty/default
95    /// value.
96    pub params: Value,
97}
98
99impl JsonRpcNotification {
100    /// Creates a new protocol::json_rpc value with explicit
101    /// caller-provided inputs. This constructor is data-only and
102    /// performs no I/O or external side effects.
103    pub fn new(method: impl Into<String>, params: Value) -> Self {
104        Self {
105            jsonrpc: "2.0".to_string(),
106            method: method.into(),
107            params,
108        }
109    }
110}
111
112#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
113/// JSON-RPC error object embedded in an error response.
114/// It carries protocol failure details only; constructing it does not log, publish, or send the
115/// error.
116pub struct JsonRpcErrorObject {
117    /// JSON-RPC error code, using standard protocol codes or an adapter-defined extension code.
118    pub code: i64,
119    /// Human-readable protocol error message.
120    pub message: String,
121    #[serde(skip_serializing_if = "Option::is_none")]
122    /// Optional data value.
123    /// When absent, callers should use the documented default or skip that optional behavior.
124    pub data: Option<Value>,
125}
126
127impl JsonRpcErrorObject {
128    /// Creates a new protocol::json_rpc value with explicit
129    /// caller-provided inputs. This constructor is data-only and
130    /// performs no I/O or external side effects.
131    pub fn new(code: i64, message: impl Into<String>) -> Self {
132        Self {
133            code,
134            message: message.into(),
135            data: None,
136        }
137    }
138}
139
140#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
141/// JSON-RPC response frame for a completed request.
142/// A response contains exactly one of `result` or `error`; sending it is handled by the endpoint
143/// or transport layer.
144pub struct JsonRpcResponse {
145    /// Protocol version marker; toolkit constructors always set this to `"2.0"`.
146    pub jsonrpc: String,
147    /// Request id this response is correlated with.
148    pub id: JsonRpcId,
149    #[serde(skip_serializing_if = "Option::is_none")]
150    /// Result payload produced by a validator, executor, sink, or adapter.
151    pub result: Option<Value>,
152    #[serde(skip_serializing_if = "Option::is_none")]
153    /// Typed error payload or redacted error detail for failed operations.
154    pub error: Option<JsonRpcErrorObject>,
155}
156
157impl JsonRpcResponse {
158    /// Builds the result value.
159    /// This is data construction and performs no I/O, journal append, event publication, or
160    /// process work.
161    pub fn result(id: JsonRpcId, result: Value) -> Self {
162        Self {
163            jsonrpc: "2.0".to_string(),
164            id,
165            result: Some(result),
166            error: None,
167        }
168    }
169
170    /// Builds the error value.
171    /// This is data construction and performs no I/O, journal append, event publication, or
172    /// process work.
173    pub fn error(id: Option<JsonRpcId>, code: i64, message: impl Into<String>) -> Self {
174        Self {
175            jsonrpc: "2.0".to_string(),
176            id: id.unwrap_or(JsonRpcId::Null),
177            result: None,
178            error: Some(JsonRpcErrorObject::new(code, message)),
179        }
180    }
181}
182
183#[derive(Clone, Debug, PartialEq)]
184/// Parsed JSON-RPC frame used by line transports and scripted protocol fakes.
185/// Matching on the frame is side-effect free; endpoint methods own any transcript or transport
186/// mutation.
187pub enum JsonRpcFrame {
188    /// Client request that expects a response.
189    Request(JsonRpcRequest),
190    /// Response to a previously sent request.
191    Response(JsonRpcResponse),
192    /// One-way notification with no response id.
193    Notification(JsonRpcNotification),
194}
195
196impl JsonRpcFrame {
197    /// Converts this value into line data.
198    /// This serializes the frame into one JSON-RPC line and performs no transport I/O.
199    pub fn to_line(&self) -> Result<String, AgentError> {
200        let value = match self {
201            Self::Request(frame) => serde_json::to_value(frame),
202            Self::Response(frame) => serde_json::to_value(frame),
203            Self::Notification(frame) => serde_json::to_value(frame),
204        }
205        .map_err(json_error)?;
206        let line = serde_json::to_string(&value).map_err(json_error)?;
207        validate_json_rpc_line(&line)?;
208        Ok(line)
209    }
210
211    /// Constructs this value from line. Use it when adapting canonical
212    /// SDK records without introducing a second behavior path.
213    pub fn from_line(line: &str) -> Result<Self, AgentError> {
214        validate_json_rpc_line(line)?;
215        let value: Value = serde_json::from_str(line).map_err(json_error)?;
216        let object = value
217            .as_object()
218            .ok_or_else(|| protocol_violation("json-rpc frame must be an object"))?;
219        match object.get("jsonrpc").and_then(Value::as_str) {
220            Some("2.0") => {}
221            _ => {
222                return Err(protocol_violation(
223                    "json-rpc frame must declare version 2.0",
224                ));
225            }
226        }
227        if object.contains_key("method") {
228            if object.contains_key("id") {
229                if object.get("id").is_some_and(Value::is_null) {
230                    return Err(protocol_violation("json-rpc request id must not be null"));
231                }
232                return serde_json::from_value(value)
233                    .map(Self::Request)
234                    .map_err(json_error);
235            }
236            return serde_json::from_value(value)
237                .map(Self::Notification)
238                .map_err(json_error);
239        }
240        if object.contains_key("result") || object.contains_key("error") {
241            if object.contains_key("result") == object.contains_key("error") {
242                return Err(protocol_violation(
243                    "json-rpc response must contain exactly one of result or error",
244                ));
245            }
246            if !object.contains_key("id") {
247                return Err(protocol_violation("json-rpc response must include id"));
248            }
249            return serde_json::from_value(value)
250                .map(Self::Response)
251                .map_err(json_error);
252        }
253        Err(protocol_violation(
254            "json-rpc frame is neither request nor response",
255        ))
256    }
257}
258
259/// Extracts a response frame or returns a protocol violation.
260/// This is a pure frame check used by scripted tests; it does not read or write a transport.
261pub(crate) fn expect_response(frame: JsonRpcFrame) -> Result<JsonRpcResponse, AgentError> {
262    match frame {
263        JsonRpcFrame::Response(response) => Ok(response),
264        _ => Err(protocol_violation("expected json-rpc response frame")),
265    }
266}
267
268/// Extracts a notification frame or returns a protocol violation.
269/// This is a pure frame check used by scripted tests; it does not read or write a transport.
270pub(crate) fn expect_notification(frame: JsonRpcFrame) -> Result<JsonRpcNotification, AgentError> {
271    match frame {
272        JsonRpcFrame::Notification(notification) => Ok(notification),
273        _ => Err(protocol_violation("expected json-rpc notification frame")),
274    }
275}
276
277/// Converts a serde JSON failure into the toolkit's protocol-violation error.
278/// The conversion only allocates an error value; callers decide whether to return, send, or log it.
279pub(crate) fn json_error(error: serde_json::Error) -> AgentError {
280    protocol_violation(format!("json-rpc serialization failed: {error}"))
281}
282
283/// Converts a stdio transport failure into the toolkit's protocol-violation error.
284/// The conversion only allocates an error value; it does not retry or touch the transport.
285pub(crate) fn stdio_error(error: std::io::Error) -> AgentError {
286    protocol_violation(format!("json-rpc stdio transport failed: {error}"))
287}
288
289/// Validates the protocol::json_rpc invariants and returns a typed
290/// error on failure. Validation is pure and does not perform I/O,
291/// dispatch, journal appends, or adapter calls.
292pub(crate) fn validate_json_rpc_line(line: &str) -> Result<(), AgentError> {
293    if line.contains('\n') || line.contains('\r') {
294        return Err(protocol_violation(
295            "json-rpc line transport frames must not contain embedded newlines",
296        ));
297    }
298    Ok(())
299}
300
301/// Creates a typed contract-violation error for malformed protocol frames.
302/// This helper is side-effect free; endpoint code decides whether the error becomes a JSON-RPC
303/// error response.
304pub(crate) fn protocol_violation(message: impl Into<String>) -> AgentError {
305    AgentError::contract_violation(message)
306}