use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const JSONRPC_VERSION: &str = "2.0";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpcRequest {
pub jsonrpc: String,
pub method: String,
#[serde(default)]
pub params: Value,
pub id: Option<RpcId>,
}
impl RpcRequest {
pub fn is_notification(&self) -> bool {
self.id.is_none()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpcResponse {
pub jsonrpc: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<RpcError>,
pub id: RpcId,
}
impl RpcResponse {
pub fn success(id: RpcId, result: Value) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_string(),
result: Some(result),
error: None,
id,
}
}
pub fn error(id: RpcId, error: RpcError) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_string(),
result: None,
error: Some(error),
id,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpcNotification {
pub jsonrpc: String,
pub method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
}
impl RpcNotification {
pub fn new(method: impl Into<String>, params: Value) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_string(),
method: method.into(),
params: Some(params),
}
}
pub fn agent_started(task_id: &str) -> Self {
Self::new(
"agent.started",
serde_json::json!({
"task_id": task_id
}),
)
}
pub fn agent_output(task_id: &str, line: &str) -> Self {
Self::new(
"agent.output",
serde_json::json!({
"task_id": task_id,
"line": line
}),
)
}
pub fn agent_completed(
task_id: &str,
success: bool,
exit_code: Option<i32>,
duration_ms: u64,
) -> Self {
Self::new(
"agent.completed",
serde_json::json!({
"task_id": task_id,
"success": success,
"exit_code": exit_code,
"duration_ms": duration_ms
}),
)
}
pub fn agent_spawn_failed(task_id: &str, error: &str) -> Self {
Self::new(
"agent.spawn_failed",
serde_json::json!({
"task_id": task_id,
"error": error
}),
)
}
pub fn server_ready(version: &str) -> Self {
Self::new(
"server.ready",
serde_json::json!({
"version": version
}),
)
}
pub fn server_shutdown() -> Self {
Self::new("server.shutdown", serde_json::json!({}))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpcError {
pub code: i32,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
impl RpcError {
pub fn new(code: i32, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
data: None,
}
}
pub fn with_data(code: i32, message: impl Into<String>, data: Value) -> Self {
Self {
code,
message: message.into(),
data: Some(data),
}
}
pub fn parse_error(msg: &str) -> Self {
Self::new(-32700, format!("Parse error: {}", msg))
}
pub fn invalid_request(msg: &str) -> Self {
Self::new(-32600, format!("Invalid request: {}", msg))
}
pub fn method_not_found(method: &str) -> Self {
Self::new(-32601, format!("Method not found: {}", method))
}
pub fn invalid_params(msg: &str) -> Self {
Self::new(-32602, format!("Invalid params: {}", msg))
}
pub fn internal_error(msg: &str) -> Self {
Self::new(-32603, format!("Internal error: {}", msg))
}
pub fn spawn_failed(msg: &str) -> Self {
Self::new(-32001, format!("Agent spawn failed: {}", msg))
}
pub fn task_not_found(task_id: &str) -> Self {
Self::new(-32002, format!("Task not found: {}", task_id))
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(untagged)]
pub enum RpcId {
String(String),
Number(i64),
#[default]
Null,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpawnParams {
pub task_id: String,
pub prompt: String,
#[serde(default)]
pub working_dir: Option<String>,
#[serde(default)]
pub harness: Option<String>,
#[serde(default)]
pub model: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpawnTaskParams {
pub task_id: String,
#[serde(default)]
pub tag: Option<String>,
#[serde(default)]
pub harness: Option<String>,
#[serde(default)]
pub model: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ListTasksParams {
#[serde(default)]
pub tag: Option<String>,
#[serde(default)]
pub status: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetTaskParams {
pub task_id: String,
#[serde(default)]
pub tag: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetStatusParams {
pub task_id: String,
pub status: String,
#[serde(default)]
pub tag: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct NextTaskParams {
#[serde(default)]
pub tag: Option<String>,
#[serde(default)]
pub all_tags: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_request() {
let json = r#"{"jsonrpc": "2.0", "method": "spawn", "params": {"task_id": "1", "prompt": "test"}, "id": 1}"#;
let req: RpcRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.method, "spawn");
assert_eq!(req.id, Some(RpcId::Number(1)));
}
#[test]
fn test_parse_notification() {
let json = r#"{"jsonrpc": "2.0", "method": "cancel", "params": {"task_id": "1"}}"#;
let req: RpcRequest = serde_json::from_str(json).unwrap();
assert!(req.is_notification());
}
#[test]
fn test_serialize_response() {
let resp = RpcResponse::success(RpcId::Number(1), serde_json::json!({"status": "ok"}));
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("\"result\""));
assert!(!json.contains("\"error\""));
}
#[test]
fn test_serialize_notification() {
let notif = RpcNotification::agent_started("task:1");
let json = serde_json::to_string(¬if).unwrap();
assert!(json.contains("agent.started"));
assert!(json.contains("task:1"));
}
#[test]
fn test_error_codes() {
let err = RpcError::method_not_found("unknown");
assert_eq!(err.code, -32601);
}
#[test]
fn test_spawn_params() {
let json = r#"{"task_id": "1", "prompt": "do something", "harness": "claude"}"#;
let params: SpawnParams = serde_json::from_str(json).unwrap();
assert_eq!(params.task_id, "1");
assert_eq!(params.prompt, "do something");
assert_eq!(params.harness, Some("claude".to_string()));
}
}