Skip to main content

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, Default)]
219#[serde(untagged)]
220pub enum RpcId {
221    String(String),
222    Number(i64),
223    #[default]
224    Null,
225}
226
227// ============================================================================
228// Request Parameter Types
229// ============================================================================
230
231/// Parameters for the "spawn" method
232#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct SpawnParams {
234    /// Task ID to spawn agent for
235    pub task_id: String,
236    /// Prompt to send to the agent
237    pub prompt: String,
238    /// Working directory (optional, defaults to current dir)
239    #[serde(default)]
240    pub working_dir: Option<String>,
241    /// Harness to use: "claude" or "opencode" (optional, defaults to config)
242    #[serde(default)]
243    pub harness: Option<String>,
244    /// Model to use (optional, defaults to config)
245    #[serde(default)]
246    pub model: Option<String>,
247}
248
249/// Parameters for the "spawn_task" method (spawns from task graph)
250#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct SpawnTaskParams {
252    /// Task ID in the task graph
253    pub task_id: String,
254    /// Phase tag (optional, uses active if not provided)
255    #[serde(default)]
256    pub tag: Option<String>,
257    /// Harness to use (optional, defaults to config or task's agent_type)
258    #[serde(default)]
259    pub harness: Option<String>,
260    /// Model to use (optional, defaults to config or task's agent_type)
261    #[serde(default)]
262    pub model: Option<String>,
263}
264
265/// Parameters for the "list_tasks" method
266#[derive(Debug, Clone, Serialize, Deserialize, Default)]
267pub struct ListTasksParams {
268    /// Phase tag (optional, uses active if not provided)
269    #[serde(default)]
270    pub tag: Option<String>,
271    /// Filter by status
272    #[serde(default)]
273    pub status: Option<String>,
274}
275
276/// Parameters for the "get_task" method
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct GetTaskParams {
279    /// Task ID
280    pub task_id: String,
281    /// Phase tag (optional, uses active if not provided)
282    #[serde(default)]
283    pub tag: Option<String>,
284}
285
286/// Parameters for the "set_status" method
287#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct SetStatusParams {
289    /// Task ID
290    pub task_id: String,
291    /// New status
292    pub status: String,
293    /// Phase tag (optional, uses active if not provided)
294    #[serde(default)]
295    pub tag: Option<String>,
296}
297
298/// Parameters for the "next_task" method
299#[derive(Debug, Clone, Serialize, Deserialize, Default)]
300pub struct NextTaskParams {
301    /// Phase tag (optional, uses active if not provided)
302    #[serde(default)]
303    pub tag: Option<String>,
304    /// Search all tags
305    #[serde(default)]
306    pub all_tags: bool,
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312
313    #[test]
314    fn test_parse_request() {
315        let json = r#"{"jsonrpc": "2.0", "method": "spawn", "params": {"task_id": "1", "prompt": "test"}, "id": 1}"#;
316        let req: RpcRequest = serde_json::from_str(json).unwrap();
317        assert_eq!(req.method, "spawn");
318        assert_eq!(req.id, Some(RpcId::Number(1)));
319    }
320
321    #[test]
322    fn test_parse_notification() {
323        let json = r#"{"jsonrpc": "2.0", "method": "cancel", "params": {"task_id": "1"}}"#;
324        let req: RpcRequest = serde_json::from_str(json).unwrap();
325        assert!(req.is_notification());
326    }
327
328    #[test]
329    fn test_serialize_response() {
330        let resp = RpcResponse::success(RpcId::Number(1), serde_json::json!({"status": "ok"}));
331        let json = serde_json::to_string(&resp).unwrap();
332        assert!(json.contains("\"result\""));
333        assert!(!json.contains("\"error\""));
334    }
335
336    #[test]
337    fn test_serialize_notification() {
338        let notif = RpcNotification::agent_started("task:1");
339        let json = serde_json::to_string(&notif).unwrap();
340        assert!(json.contains("agent.started"));
341        assert!(json.contains("task:1"));
342    }
343
344    #[test]
345    fn test_error_codes() {
346        let err = RpcError::method_not_found("unknown");
347        assert_eq!(err.code, -32601);
348    }
349
350    #[test]
351    fn test_spawn_params() {
352        let json = r#"{"task_id": "1", "prompt": "do something", "harness": "claude"}"#;
353        let params: SpawnParams = serde_json::from_str(json).unwrap();
354        assert_eq!(params.task_id, "1");
355        assert_eq!(params.prompt, "do something");
356        assert_eq!(params.harness, Some("claude".to_string()));
357    }
358}