1use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6pub const PROTOCOL_VERSION: u32 = 2;
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
9#[serde(rename_all = "snake_case")]
10pub enum ComponentState {
11 Disabled,
12 Starting,
13 Connected,
14 Disconnected,
15 Backoff,
16 Stopping,
17 Stopped,
18 Failed,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22pub struct ComponentHealth {
23 pub id: String,
24 pub account: String,
25 pub bot_id: Option<String>,
26 pub user_id: Option<String>,
27 pub enabled: bool,
28 pub state: ComponentState,
29 pub last_success_unix_seconds: Option<u64>,
30 pub error: Option<String>,
31 pub restarts: u64,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
35pub struct DaemonStatus {
36 pub version: String,
37 pub pid: u32,
38 pub components: Vec<ComponentHealth>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
42#[serde(tag = "action", rename_all = "snake_case")]
43pub enum DaemonCommand {
44 Status,
45 Reload,
46 ClawbotSet {
47 account: String,
48 enabled: bool,
49 workspace: Option<String>,
50 },
51 ClawbotLogout {
52 account: String,
53 },
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
57pub struct QueueEntry {
58 pub queue_id: String,
59 pub revision: u64,
60 pub prompt: String,
61 pub submitter: String,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
65pub struct PeerInfo {
66 pub name: String,
67 pub version: String,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
71pub struct Usage {
72 #[serde(skip_serializing_if = "Option::is_none")]
73 pub input_tokens: Option<u64>,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 pub output_tokens: Option<u64>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
79#[serde(tag = "type")]
80pub enum ClientMessage {
81 #[serde(rename = "daemon.control")]
82 DaemonControl {
83 request_id: String,
84 command: DaemonCommand,
85 },
86 #[serde(rename = "initialize")]
87 Initialize {
88 request_id: String,
89 protocol_version: u32,
90 client: PeerInfo,
91 },
92 #[serde(rename = "session.start")]
93 SessionStart {
94 request_id: String,
95 cwd: String,
96 #[serde(default, skip_serializing_if = "Option::is_none")]
97 provider: Option<String>,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 model: Option<String>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
101 base_url: Option<String>,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
103 no_tools: Option<bool>,
104 },
105 #[serde(rename = "session.attach")]
106 SessionAttach {
107 request_id: String,
108 session_id: String,
109 cwd: String,
110 },
111 #[serde(rename = "turn.start")]
112 TurnStart {
113 request_id: String,
114 session_id: String,
115 prompt: String,
116 },
117 #[serde(rename = "queue.update")]
118 QueueUpdate {
119 request_id: String,
120 session_id: String,
121 queue_id: String,
122 revision: u64,
123 prompt: String,
124 },
125 #[serde(rename = "queue.move")]
126 QueueMove {
127 request_id: String,
128 session_id: String,
129 queue_id: String,
130 revision: u64,
131 before_queue_id: Option<String>,
132 },
133 #[serde(rename = "queue.remove")]
134 QueueRemove {
135 request_id: String,
136 session_id: String,
137 queue_id: String,
138 revision: u64,
139 },
140 #[serde(rename = "session.pause")]
141 SessionPause {
142 request_id: String,
143 session_id: String,
144 paused: bool,
145 },
146 #[serde(rename = "turn.cancel")]
147 TurnCancel {
148 request_id: String,
149 session_id: String,
150 turn_id: String,
151 },
152 #[serde(rename = "approval.resolve")]
153 ApprovalResolve {
154 request_id: String,
155 session_id: String,
156 approval_id: String,
157 approved: bool,
158 },
159 #[serde(rename = "session.clear")]
160 SessionClear {
161 request_id: String,
162 session_id: String,
163 },
164}
165
166impl ClientMessage {
167 pub fn request_id(&self) -> &str {
168 match self {
169 Self::Initialize { request_id, .. }
170 | Self::DaemonControl { request_id, .. }
171 | Self::SessionStart { request_id, .. }
172 | Self::SessionAttach { request_id, .. }
173 | Self::TurnStart { request_id, .. }
174 | Self::QueueUpdate { request_id, .. }
175 | Self::QueueMove { request_id, .. }
176 | Self::QueueRemove { request_id, .. }
177 | Self::SessionPause { request_id, .. }
178 | Self::TurnCancel { request_id, .. }
179 | Self::ApprovalResolve { request_id, .. }
180 | Self::SessionClear { request_id, .. } => request_id,
181 }
182 }
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
186#[serde(tag = "type")]
187pub enum ServerEvent {
188 #[serde(rename = "daemon.status")]
189 DaemonStatus {
190 request_id: String,
191 status: DaemonStatus,
192 },
193 #[serde(rename = "initialized")]
194 Initialized {
195 request_id: String,
196 protocol_version: u32,
197 server: PeerInfo,
198 },
199 #[serde(rename = "session.started")]
200 SessionStarted {
201 request_id: String,
202 session_id: String,
203 cwd: String,
204 model: String,
205 context_max_tokens: usize,
206 max_server_frame_bytes: usize,
207 max_transcript_bytes: usize,
208 max_transcript_items: usize,
209 max_prompt_history_bytes: usize,
210 max_prompt_history_items: usize,
211 },
212 #[serde(rename = "queue.snapshot")]
213 QueueSnapshot {
214 request_id: Option<String>,
215 session_id: String,
216 seq: u64,
217 entries: Vec<QueueEntry>,
218 paused: bool,
219 },
220 #[serde(rename = "queue.enqueued")]
221 QueueEnqueued {
222 request_id: String,
223 session_id: String,
224 seq: u64,
225 entry: QueueEntry,
226 position: usize,
227 },
228 #[serde(rename = "queue.updated")]
229 QueueUpdated {
230 request_id: String,
231 session_id: String,
232 seq: u64,
233 entry: QueueEntry,
234 },
235 #[serde(rename = "queue.moved")]
236 QueueMoved {
237 request_id: String,
238 session_id: String,
239 seq: u64,
240 queue_id: String,
241 position: usize,
242 revision: u64,
243 },
244 #[serde(rename = "queue.removed")]
245 QueueRemoved {
246 request_id: String,
247 session_id: String,
248 seq: u64,
249 queue_id: String,
250 revision: u64,
251 },
252 #[serde(rename = "queue.dequeued")]
253 QueueDequeued {
254 request_id: String,
255 session_id: String,
256 seq: u64,
257 queue_id: String,
258 turn_id: String,
259 },
260 #[serde(rename = "session.paused")]
261 SessionPaused {
262 request_id: String,
263 session_id: String,
264 seq: u64,
265 paused: bool,
266 },
267 #[serde(rename = "turn.started")]
268 TurnStarted {
269 request_id: String,
270 session_id: String,
271 turn_id: String,
272 seq: u64,
273 },
274 #[serde(rename = "assistant.delta")]
275 AssistantDelta {
276 request_id: String,
277 session_id: String,
278 turn_id: String,
279 seq: u64,
280 content: String,
281 },
282 #[serde(rename = "assistant.completed")]
283 AssistantCompleted {
284 request_id: String,
285 session_id: String,
286 turn_id: String,
287 seq: u64,
288 content: String,
289 },
290 #[serde(rename = "tool.proposed")]
291 ToolProposed {
292 request_id: String,
293 session_id: String,
294 turn_id: String,
295 seq: u64,
296 call_id: String,
297 name: String,
298 arguments: Value,
299 },
300 #[serde(rename = "approval.requested")]
301 ApprovalRequested {
302 request_id: String,
303 session_id: String,
304 turn_id: String,
305 seq: u64,
306 approval_id: String,
307 call_id: String,
308 name: String,
309 risk: String,
310 cwd: String,
311 summary: String,
312 },
313 #[serde(rename = "tool.started")]
314 ToolStarted {
315 request_id: String,
316 session_id: String,
317 turn_id: String,
318 seq: u64,
319 call_id: String,
320 name: String,
321 },
322 #[serde(rename = "tool.completed")]
323 ToolCompleted {
324 request_id: String,
325 session_id: String,
326 turn_id: String,
327 seq: u64,
328 call_id: String,
329 name: String,
330 success: bool,
331 output: String,
332 truncated: bool,
333 },
334 #[serde(rename = "context.compacted")]
335 ContextCompacted {
336 request_id: String,
337 session_id: String,
338 turn_id: String,
339 seq: u64,
340 before_tokens: usize,
341 after_tokens: usize,
342 removed_messages: usize,
343 },
344 #[serde(rename = "session.trimmed")]
345 SessionTrimmed {
346 request_id: String,
347 session_id: String,
348 seq: u64,
349 removed_messages: usize,
350 history_bytes: usize,
351 },
352 #[serde(rename = "session.cleared")]
353 SessionCleared {
354 request_id: String,
355 session_id: String,
356 seq: u64,
357 },
358 #[serde(rename = "turn.completed")]
359 TurnCompleted {
360 request_id: String,
361 session_id: String,
362 turn_id: String,
363 seq: u64,
364 steps: usize,
365 usage: Usage,
366 },
367 #[serde(rename = "turn.cancelled")]
368 TurnCancelled {
369 request_id: String,
370 session_id: String,
371 turn_id: String,
372 seq: u64,
373 },
374 #[serde(rename = "turn.failed")]
375 TurnFailed {
376 request_id: String,
377 session_id: String,
378 turn_id: String,
379 seq: u64,
380 code: String,
381 message: String,
382 },
383 #[serde(rename = "error")]
384 Error {
385 #[serde(skip_serializing_if = "Option::is_none")]
386 request_id: Option<String>,
387 code: String,
388 message: String,
389 fatal: bool,
390 },
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 #[test]
398 fn client_message_round_trip() {
399 let message = ClientMessage::TurnStart {
400 request_id: "3".into(),
401 session_id: "session".into(),
402 prompt: "hello".into(),
403 };
404 let json = serde_json::to_string(&message).unwrap();
405 assert!(json.contains("\"type\":\"turn.start\""));
406 assert_eq!(
407 serde_json::from_str::<ClientMessage>(&json).unwrap(),
408 message
409 );
410 }
411
412 #[test]
413 fn additive_fields_are_ignored() {
414 let json = r#"{"type":"session.clear","request_id":"1","session_id":"s","future":true}"#;
415 assert!(matches!(
416 serde_json::from_str::<ClientMessage>(json).unwrap(),
417 ClientMessage::SessionClear { .. }
418 ));
419 }
420
421 #[test]
422 fn event_round_trip() {
423 let event = ServerEvent::AssistantDelta {
424 request_id: "1".into(),
425 session_id: "s".into(),
426 turn_id: "t".into(),
427 seq: 4,
428 content: "hello".into(),
429 };
430 let encoded = serde_json::to_string(&event).unwrap();
431 assert_eq!(
432 serde_json::from_str::<ServerEvent>(&encoded).unwrap(),
433 event
434 );
435 }
436
437 #[test]
438 fn queue_messages_and_events_round_trip() {
439 let message = ClientMessage::QueueMove {
440 request_id: "q1".into(),
441 session_id: "s".into(),
442 queue_id: "q".into(),
443 revision: 2,
444 before_queue_id: None,
445 };
446 let encoded = serde_json::to_string(&message).unwrap();
447 assert_eq!(
448 serde_json::from_str::<ClientMessage>(&encoded).unwrap(),
449 message
450 );
451 let event = ServerEvent::QueueSnapshot {
452 request_id: None,
453 session_id: "s".into(),
454 seq: 4,
455 entries: vec![QueueEntry {
456 queue_id: "q".into(),
457 revision: 1,
458 prompt: "hello".into(),
459 submitter: "cli".into(),
460 }],
461 paused: false,
462 };
463 let encoded = serde_json::to_string(&event).unwrap();
464 assert_eq!(
465 serde_json::from_str::<ServerEvent>(&encoded).unwrap(),
466 event
467 );
468 }
469}