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