1use 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}