Skip to main content

scv_protocol/
lib.rs

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