scud/rpc/
types.rs

1//! JSON RPC 2.0 message types for IPC communication
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6/// JSON RPC version constant
7pub const JSONRPC_VERSION: &str = "2.0";
8
9/// JSON RPC Request (incoming from stdin)
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct RpcRequest {
12    /// JSON RPC version, must be "2.0"
13    pub jsonrpc: String,
14    /// Method name to invoke
15    pub method: String,
16    /// Method parameters (optional)
17    #[serde(default)]
18    pub params: Value,
19    /// Request ID (optional for notifications)
20    pub id: Option<RpcId>,
21}
22
23impl RpcRequest {
24    /// Check if this request is a notification (no id)
25    pub fn is_notification(&self) -> bool {
26        self.id.is_none()
27    }
28}
29
30/// JSON RPC Response (outgoing to stdout)
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct RpcResponse {
33    /// JSON RPC version, always "2.0"
34    pub jsonrpc: String,
35    /// Success result (mutually exclusive with error)
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub result: Option<Value>,
38    /// Error result (mutually exclusive with result)
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub error: Option<RpcError>,
41    /// Request ID this is responding to
42    pub id: RpcId,
43}
44
45impl RpcResponse {
46    /// Create a success response
47    pub fn success(id: RpcId, result: Value) -> Self {
48        Self {
49            jsonrpc: JSONRPC_VERSION.to_string(),
50            result: Some(result),
51            error: None,
52            id,
53        }
54    }
55
56    /// Create an error response
57    pub fn error(id: RpcId, error: RpcError) -> Self {
58        Self {
59            jsonrpc: JSONRPC_VERSION.to_string(),
60            result: None,
61            error: Some(error),
62            id,
63        }
64    }
65}
66
67/// JSON RPC Notification (outgoing event, no id expected)
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct RpcNotification {
70    /// JSON RPC version, always "2.0"
71    pub jsonrpc: String,
72    /// Event method name
73    pub method: String,
74    /// Event parameters
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub params: Option<Value>,
77}
78
79impl RpcNotification {
80    /// Create a new notification
81    pub fn new(method: impl Into<String>, params: Value) -> Self {
82        Self {
83            jsonrpc: JSONRPC_VERSION.to_string(),
84            method: method.into(),
85            params: Some(params),
86        }
87    }
88
89    /// Create agent started event
90    pub fn agent_started(task_id: &str) -> Self {
91        Self::new(
92            "agent.started",
93            serde_json::json!({
94                "task_id": task_id
95            }),
96        )
97    }
98
99    /// Create agent output event
100    pub fn agent_output(task_id: &str, line: &str) -> Self {
101        Self::new(
102            "agent.output",
103            serde_json::json!({
104                "task_id": task_id,
105                "line": line
106            }),
107        )
108    }
109
110    /// Create agent completed event
111    pub fn agent_completed(
112        task_id: &str,
113        success: bool,
114        exit_code: Option<i32>,
115        duration_ms: u64,
116    ) -> Self {
117        Self::new(
118            "agent.completed",
119            serde_json::json!({
120                "task_id": task_id,
121                "success": success,
122                "exit_code": exit_code,
123                "duration_ms": duration_ms
124            }),
125        )
126    }
127
128    /// Create agent spawn failed event
129    pub fn agent_spawn_failed(task_id: &str, error: &str) -> Self {
130        Self::new(
131            "agent.spawn_failed",
132            serde_json::json!({
133                "task_id": task_id,
134                "error": error
135            }),
136        )
137    }
138
139    /// Create server ready event
140    pub fn server_ready(version: &str) -> Self {
141        Self::new(
142            "server.ready",
143            serde_json::json!({
144                "version": version
145            }),
146        )
147    }
148
149    /// Create server shutdown event
150    pub fn server_shutdown() -> Self {
151        Self::new("server.shutdown", serde_json::json!({}))
152    }
153}
154
155/// JSON RPC Error object
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct RpcError {
158    /// Error code
159    pub code: i32,
160    /// Error message
161    pub message: String,
162    /// Additional error data
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub data: Option<Value>,
165}
166
167impl RpcError {
168    /// Create a new error
169    pub fn new(code: i32, message: impl Into<String>) -> Self {
170        Self {
171            code,
172            message: message.into(),
173            data: None,
174        }
175    }
176
177    /// Create a new error with data
178    pub fn with_data(code: i32, message: impl Into<String>, data: Value) -> Self {
179        Self {
180            code,
181            message: message.into(),
182            data: Some(data),
183        }
184    }
185
186    // Standard JSON RPC error codes
187    pub fn parse_error(msg: &str) -> Self {
188        Self::new(-32700, format!("Parse error: {}", msg))
189    }
190
191    pub fn invalid_request(msg: &str) -> Self {
192        Self::new(-32600, format!("Invalid request: {}", msg))
193    }
194
195    pub fn method_not_found(method: &str) -> Self {
196        Self::new(-32601, format!("Method not found: {}", method))
197    }
198
199    pub fn invalid_params(msg: &str) -> Self {
200        Self::new(-32602, format!("Invalid params: {}", msg))
201    }
202
203    pub fn internal_error(msg: &str) -> Self {
204        Self::new(-32603, format!("Internal error: {}", msg))
205    }
206
207    // Custom error codes (application-specific, use -32000 to -32099)
208    pub fn spawn_failed(msg: &str) -> Self {
209        Self::new(-32001, format!("Agent spawn failed: {}", msg))
210    }
211
212    pub fn task_not_found(task_id: &str) -> Self {
213        Self::new(-32002, format!("Task not found: {}", task_id))
214    }
215}
216
217/// JSON RPC ID can be string, number, or null
218#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
219#[serde(untagged)]
220pub enum RpcId {
221    String(String),
222    Number(i64),
223    Null,
224}
225
226impl Default for RpcId {
227    fn default() -> Self {
228        Self::Null
229    }
230}
231
232// ============================================================================
233// Request Parameter Types
234// ============================================================================
235
236/// Parameters for the "spawn" method
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct SpawnParams {
239    /// Task ID to spawn agent for
240    pub task_id: String,
241    /// Prompt to send to the agent
242    pub prompt: String,
243    /// Working directory (optional, defaults to current dir)
244    #[serde(default)]
245    pub working_dir: Option<String>,
246    /// Harness to use: "claude" or "opencode" (optional, defaults to config)
247    #[serde(default)]
248    pub harness: Option<String>,
249    /// Model to use (optional, defaults to config)
250    #[serde(default)]
251    pub model: Option<String>,
252}
253
254/// Parameters for the "spawn_task" method (spawns from task graph)
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct SpawnTaskParams {
257    /// Task ID in the task graph
258    pub task_id: String,
259    /// Phase tag (optional, uses active if not provided)
260    #[serde(default)]
261    pub tag: Option<String>,
262    /// Harness to use (optional, defaults to config or task's agent_type)
263    #[serde(default)]
264    pub harness: Option<String>,
265    /// Model to use (optional, defaults to config or task's agent_type)
266    #[serde(default)]
267    pub model: Option<String>,
268}
269
270/// Parameters for the "list_tasks" method
271#[derive(Debug, Clone, Serialize, Deserialize, Default)]
272pub struct ListTasksParams {
273    /// Phase tag (optional, uses active if not provided)
274    #[serde(default)]
275    pub tag: Option<String>,
276    /// Filter by status
277    #[serde(default)]
278    pub status: Option<String>,
279}
280
281/// Parameters for the "get_task" method
282#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct GetTaskParams {
284    /// Task ID
285    pub task_id: String,
286    /// Phase tag (optional, uses active if not provided)
287    #[serde(default)]
288    pub tag: Option<String>,
289}
290
291/// Parameters for the "set_status" method
292#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct SetStatusParams {
294    /// Task ID
295    pub task_id: String,
296    /// New status
297    pub status: String,
298    /// Phase tag (optional, uses active if not provided)
299    #[serde(default)]
300    pub tag: Option<String>,
301}
302
303/// Parameters for the "next_task" method
304#[derive(Debug, Clone, Serialize, Deserialize, Default)]
305pub struct NextTaskParams {
306    /// Phase tag (optional, uses active if not provided)
307    #[serde(default)]
308    pub tag: Option<String>,
309    /// Search all tags
310    #[serde(default)]
311    pub all_tags: bool,
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317
318    #[test]
319    fn test_parse_request() {
320        let json = r#"{"jsonrpc": "2.0", "method": "spawn", "params": {"task_id": "1", "prompt": "test"}, "id": 1}"#;
321        let req: RpcRequest = serde_json::from_str(json).unwrap();
322        assert_eq!(req.method, "spawn");
323        assert_eq!(req.id, Some(RpcId::Number(1)));
324    }
325
326    #[test]
327    fn test_parse_notification() {
328        let json = r#"{"jsonrpc": "2.0", "method": "cancel", "params": {"task_id": "1"}}"#;
329        let req: RpcRequest = serde_json::from_str(json).unwrap();
330        assert!(req.is_notification());
331    }
332
333    #[test]
334    fn test_serialize_response() {
335        let resp = RpcResponse::success(RpcId::Number(1), serde_json::json!({"status": "ok"}));
336        let json = serde_json::to_string(&resp).unwrap();
337        assert!(json.contains("\"result\""));
338        assert!(!json.contains("\"error\""));
339    }
340
341    #[test]
342    fn test_serialize_notification() {
343        let notif = RpcNotification::agent_started("task:1");
344        let json = serde_json::to_string(&notif).unwrap();
345        assert!(json.contains("agent.started"));
346        assert!(json.contains("task:1"));
347    }
348
349    #[test]
350    fn test_error_codes() {
351        let err = RpcError::method_not_found("unknown");
352        assert_eq!(err.code, -32601);
353    }
354
355    #[test]
356    fn test_spawn_params() {
357        let json = r#"{"task_id": "1", "prompt": "do something", "harness": "claude"}"#;
358        let params: SpawnParams = serde_json::from_str(json).unwrap();
359        assert_eq!(params.task_id, "1");
360        assert_eq!(params.prompt, "do something");
361        assert_eq!(params.harness, Some("claude".to_string()));
362    }
363}