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