Skip to main content

scv_protocol/
lib.rs

1//! Dependency-light wire types shared by SCV clients and the server.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6pub const PROTOCOL_VERSION: u32 = 2;
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
9pub struct QueueEntry {
10    pub queue_id: String,
11    pub revision: u64,
12    pub prompt: String,
13    pub submitter: String,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
17pub struct PeerInfo {
18    pub name: String,
19    pub version: String,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
23pub struct Usage {
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub input_tokens: Option<u64>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub output_tokens: Option<u64>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
31#[serde(tag = "type")]
32pub enum ClientMessage {
33    #[serde(rename = "initialize")]
34    Initialize {
35        request_id: String,
36        protocol_version: u32,
37        client: PeerInfo,
38    },
39    #[serde(rename = "session.start")]
40    SessionStart { request_id: String, cwd: String },
41    #[serde(rename = "session.attach")]
42    SessionAttach { request_id: String, session_id: String, cwd: String },
43    #[serde(rename = "turn.start")]
44    TurnStart {
45        request_id: String,
46        session_id: String,
47        prompt: String,
48    },
49    #[serde(rename = "queue.update")]
50    QueueUpdate { request_id: String, session_id: String, queue_id: String, revision: u64, prompt: String },
51    #[serde(rename = "queue.move")]
52    QueueMove { request_id: String, session_id: String, queue_id: String, revision: u64, before_queue_id: Option<String> },
53    #[serde(rename = "queue.remove")]
54    QueueRemove { request_id: String, session_id: String, queue_id: String, revision: u64 },
55    #[serde(rename = "session.pause")]
56    SessionPause { request_id: String, session_id: String, paused: bool },
57    #[serde(rename = "turn.cancel")]
58    TurnCancel {
59        request_id: String,
60        session_id: String,
61        turn_id: String,
62    },
63    #[serde(rename = "approval.resolve")]
64    ApprovalResolve {
65        request_id: String,
66        session_id: String,
67        approval_id: String,
68        approved: bool,
69    },
70    #[serde(rename = "session.clear")]
71    SessionClear {
72        request_id: String,
73        session_id: String,
74    },
75}
76
77impl ClientMessage {
78    pub fn request_id(&self) -> &str {
79        match self {
80            Self::Initialize { request_id, .. }
81            | Self::SessionStart { request_id, .. }
82            | Self::SessionAttach { request_id, .. }
83            | Self::TurnStart { request_id, .. }
84            | Self::QueueUpdate { request_id, .. }
85            | Self::QueueMove { request_id, .. }
86            | Self::QueueRemove { request_id, .. }
87            | Self::SessionPause { request_id, .. }
88            | Self::TurnCancel { request_id, .. }
89            | Self::ApprovalResolve { request_id, .. }
90            | Self::SessionClear { request_id, .. } => request_id,
91        }
92    }
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
96#[serde(tag = "type")]
97pub enum ServerEvent {
98    #[serde(rename = "initialized")]
99    Initialized {
100        request_id: String,
101        protocol_version: u32,
102        server: PeerInfo,
103    },
104    #[serde(rename = "session.started")]
105    SessionStarted {
106        request_id: String,
107        session_id: String,
108        cwd: String,
109        model: String,
110        context_max_tokens: usize,
111        max_server_frame_bytes: usize,
112        max_transcript_bytes: usize,
113        max_transcript_items: usize,
114        max_prompt_history_bytes: usize,
115        max_prompt_history_items: usize,
116    },
117    #[serde(rename = "queue.snapshot")]
118    QueueSnapshot { request_id: Option<String>, session_id: String, seq: u64, entries: Vec<QueueEntry>, paused: bool },
119    #[serde(rename = "queue.enqueued")]
120    QueueEnqueued { request_id: String, session_id: String, seq: u64, entry: QueueEntry, position: usize },
121    #[serde(rename = "queue.updated")]
122    QueueUpdated { request_id: String, session_id: String, seq: u64, entry: QueueEntry },
123    #[serde(rename = "queue.moved")]
124    QueueMoved { request_id: String, session_id: String, seq: u64, queue_id: String, position: usize, revision: u64 },
125    #[serde(rename = "queue.removed")]
126    QueueRemoved { request_id: String, session_id: String, seq: u64, queue_id: String, revision: u64 },
127    #[serde(rename = "queue.dequeued")]
128    QueueDequeued { request_id: String, session_id: String, seq: u64, queue_id: String, turn_id: String },
129    #[serde(rename = "session.paused")]
130    SessionPaused { request_id: String, session_id: String, seq: u64, paused: bool },
131    #[serde(rename = "turn.started")]
132    TurnStarted {
133        request_id: String,
134        session_id: String,
135        turn_id: String,
136        seq: u64,
137    },
138    #[serde(rename = "assistant.delta")]
139    AssistantDelta {
140        request_id: String,
141        session_id: String,
142        turn_id: String,
143        seq: u64,
144        content: String,
145    },
146    #[serde(rename = "assistant.completed")]
147    AssistantCompleted {
148        request_id: String,
149        session_id: String,
150        turn_id: String,
151        seq: u64,
152        content: String,
153    },
154    #[serde(rename = "tool.proposed")]
155    ToolProposed {
156        request_id: String,
157        session_id: String,
158        turn_id: String,
159        seq: u64,
160        call_id: String,
161        name: String,
162        arguments: Value,
163    },
164    #[serde(rename = "approval.requested")]
165    ApprovalRequested {
166        request_id: String,
167        session_id: String,
168        turn_id: String,
169        seq: u64,
170        approval_id: String,
171        call_id: String,
172        name: String,
173        risk: String,
174        cwd: String,
175        summary: String,
176    },
177    #[serde(rename = "tool.started")]
178    ToolStarted {
179        request_id: String,
180        session_id: String,
181        turn_id: String,
182        seq: u64,
183        call_id: String,
184        name: String,
185    },
186    #[serde(rename = "tool.completed")]
187    ToolCompleted {
188        request_id: String,
189        session_id: String,
190        turn_id: String,
191        seq: u64,
192        call_id: String,
193        name: String,
194        success: bool,
195        output: String,
196        truncated: bool,
197    },
198    #[serde(rename = "context.compacted")]
199    ContextCompacted {
200        request_id: String,
201        session_id: String,
202        turn_id: String,
203        seq: u64,
204        before_tokens: usize,
205        after_tokens: usize,
206        removed_messages: usize,
207    },
208    #[serde(rename = "session.trimmed")]
209    SessionTrimmed {
210        request_id: String,
211        session_id: String,
212        seq: u64,
213        removed_messages: usize,
214        history_bytes: usize,
215    },
216    #[serde(rename = "session.cleared")]
217    SessionCleared {
218        request_id: String,
219        session_id: String,
220        seq: u64,
221    },
222    #[serde(rename = "turn.completed")]
223    TurnCompleted {
224        request_id: String,
225        session_id: String,
226        turn_id: String,
227        seq: u64,
228        steps: usize,
229        usage: Usage,
230    },
231    #[serde(rename = "turn.cancelled")]
232    TurnCancelled {
233        request_id: String,
234        session_id: String,
235        turn_id: String,
236        seq: u64,
237    },
238    #[serde(rename = "turn.failed")]
239    TurnFailed {
240        request_id: String,
241        session_id: String,
242        turn_id: String,
243        seq: u64,
244        code: String,
245        message: String,
246    },
247    #[serde(rename = "error")]
248    Error {
249        #[serde(skip_serializing_if = "Option::is_none")]
250        request_id: Option<String>,
251        code: String,
252        message: String,
253        fatal: bool,
254    },
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn client_message_round_trip() {
263        let message = ClientMessage::TurnStart {
264            request_id: "3".into(),
265            session_id: "session".into(),
266            prompt: "hello".into(),
267        };
268        let json = serde_json::to_string(&message).unwrap();
269        assert!(json.contains("\"type\":\"turn.start\""));
270        assert_eq!(
271            serde_json::from_str::<ClientMessage>(&json).unwrap(),
272            message
273        );
274    }
275
276    #[test]
277    fn additive_fields_are_ignored() {
278        let json = r#"{"type":"session.clear","request_id":"1","session_id":"s","future":true}"#;
279        assert!(matches!(
280            serde_json::from_str::<ClientMessage>(json).unwrap(),
281            ClientMessage::SessionClear { .. }
282        ));
283    }
284
285    #[test]
286    fn event_round_trip() {
287        let event = ServerEvent::AssistantDelta {
288            request_id: "1".into(),
289            session_id: "s".into(),
290            turn_id: "t".into(),
291            seq: 4,
292            content: "hello".into(),
293        };
294        let encoded = serde_json::to_string(&event).unwrap();
295        assert_eq!(
296            serde_json::from_str::<ServerEvent>(&encoded).unwrap(),
297            event
298        );
299    }
300
301    #[test]
302    fn queue_messages_and_events_round_trip() {
303        let message = ClientMessage::QueueMove {
304            request_id: "q1".into(), session_id: "s".into(), queue_id: "q".into(),
305            revision: 2, before_queue_id: None,
306        };
307        let encoded = serde_json::to_string(&message).unwrap();
308        assert_eq!(serde_json::from_str::<ClientMessage>(&encoded).unwrap(), message);
309        let event = ServerEvent::QueueSnapshot {
310            request_id: None, session_id: "s".into(), seq: 4,
311            entries: vec![QueueEntry { queue_id: "q".into(), revision: 1, prompt: "hello".into(), submitter: "cli".into() }],
312            paused: false,
313        };
314        let encoded = serde_json::to_string(&event).unwrap();
315        assert_eq!(serde_json::from_str::<ServerEvent>(&encoded).unwrap(), event);
316    }
317}