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 { 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}