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}