use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize)]
pub struct JsonRpcRequest {
pub id: serde_json::Value,
pub method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<serde_json::Value>,
}
impl JsonRpcRequest {
pub fn new(id: u64, method: &str, params: Option<serde_json::Value>) -> Self {
Self {
id: serde_json::Value::Number(id.into()),
method: method.into(),
params,
}
}
}
#[derive(Debug, Serialize)]
pub struct JsonRpcClientNotification {
pub method: String,
}
impl JsonRpcClientNotification {
pub fn new(method: &str) -> Self {
Self {
method: method.into(),
}
}
}
#[derive(Debug, Deserialize)]
pub struct JsonRpcResponse {
pub id: serde_json::Value,
#[serde(default)]
pub result: Option<serde_json::Value>,
#[serde(default)]
pub error: Option<JsonRpcError>,
}
#[derive(Debug, Deserialize)]
pub struct JsonRpcError {
pub code: i64,
pub message: String,
#[serde(default)]
pub data: Option<serde_json::Value>,
}
impl std::fmt::Display for JsonRpcError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "JSON-RPC error {}: {}", self.code, self.message)
}
}
#[derive(Debug, Deserialize)]
pub struct JsonRpcNotification {
pub method: String,
#[serde(default)]
pub params: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum JsonRpcMessage {
Response(JsonRpcResponse),
Notification(JsonRpcNotification),
}
pub const METHOD_INITIALIZE: &str = "initialize";
pub const METHOD_INITIALIZED: &str = "initialized";
pub const METHOD_THREAD_START: &str = "thread/start";
pub const METHOD_TURN_START: &str = "turn/start";
pub const METHOD_TURN_INTERRUPT: &str = "turn/interrupt";
pub const EVENT_THREAD_STARTED: &str = "thread/started";
pub const EVENT_TURN_STARTED: &str = "turn/started";
pub const EVENT_TURN_COMPLETED: &str = "turn/completed";
pub const EVENT_ITEM_STARTED: &str = "item/started";
pub const EVENT_AGENT_MESSAGE_DELTA: &str = "item/agentMessage/delta";
pub const EVENT_ITEM_COMPLETED: &str = "item/completed";
pub const EVENT_COMMAND_OUTPUT_DELTA: &str = "item/commandExecution/outputDelta";
pub const EVENT_ERROR: &str = "error";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serialize_request() {
let req = JsonRpcRequest::new(
1,
METHOD_INITIALIZE,
Some(serde_json::json!({"clientInfo": {"name": "test", "version": "0.1.0"}})),
);
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains(r#""id":1"#));
assert!(json.contains(r#""method":"initialize""#));
assert!(!json.contains("jsonrpc"));
}
#[test]
fn serialize_client_notification() {
let notif = JsonRpcClientNotification::new(METHOD_INITIALIZED);
let json = serde_json::to_string(¬if).unwrap();
assert!(json.contains(r#""method":"initialized""#));
assert!(!json.contains("id"));
}
#[test]
fn deserialize_response_ok() {
let json = r#"{"id":1,"result":{"userAgent":"agent-teams/0.87.0"}}"#;
let resp: JsonRpcResponse = serde_json::from_str(json).unwrap();
assert!(resp.error.is_none());
assert_eq!(
resp.result.unwrap()["userAgent"].as_str().unwrap(),
"agent-teams/0.87.0"
);
}
#[test]
fn deserialize_response_err() {
let json = r#"{"id":2,"error":{"code":-32600,"message":"bad request"}}"#;
let resp: JsonRpcResponse = serde_json::from_str(json).unwrap();
assert!(resp.result.is_none());
let err = resp.error.unwrap();
assert_eq!(err.code, -32600);
}
#[test]
fn deserialize_notification() {
let json =
r#"{"method":"item/agentMessage/delta","params":{"delta":"hello","threadId":"t1","turnId":"0","itemId":"i1"}}"#;
let notif: JsonRpcNotification = serde_json::from_str(json).unwrap();
assert_eq!(notif.method, EVENT_AGENT_MESSAGE_DELTA);
assert_eq!(notif.params.unwrap()["delta"].as_str().unwrap(), "hello");
}
#[test]
fn deserialize_envelope_response() {
let json = r#"{"id":1,"result":null}"#;
let msg: JsonRpcMessage = serde_json::from_str(json).unwrap();
assert!(matches!(msg, JsonRpcMessage::Response(_)));
}
#[test]
fn deserialize_envelope_notification() {
let json = r#"{"method":"turn/completed","params":{"threadId":"t1","turn":{"id":"0","items":[],"status":"completed"}}}"#;
let msg: JsonRpcMessage = serde_json::from_str(json).unwrap();
assert!(matches!(msg, JsonRpcMessage::Notification(_)));
}
#[test]
fn deserialize_thread_start_response() {
let json = r#"{"id":2,"result":{"thread":{"id":"abc-123","preview":"","modelProvider":"openai","createdAt":1700000000,"path":"/tmp","source":"cli","turns":[],"cwd":"/tmp","cliVersion":"0.87.0"},"model":"gpt-4","modelProvider":"openai","cwd":"/tmp","approvalPolicy":"never","sandbox":{"type":"workspaceWrite"}}}"#;
let resp: JsonRpcResponse = serde_json::from_str(json).unwrap();
let result = resp.result.unwrap();
let thread_id = result["thread"]["id"].as_str().unwrap();
assert_eq!(thread_id, "abc-123");
}
#[test]
fn deserialize_turn_start_response() {
let json = r#"{"id":3,"result":{"turn":{"id":"0","items":[],"status":"inProgress","error":null}}}"#;
let resp: JsonRpcResponse = serde_json::from_str(json).unwrap();
let result = resp.result.unwrap();
let turn_id = result["turn"]["id"].as_str().unwrap();
assert_eq!(turn_id, "0");
}
}