1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use uuid::Uuid;
4
5#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
6pub enum Role {
7 Assistant,
8 User,
9}
10
11#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
12pub enum RuntimeMode {
13 Agent,
14 Plan,
15}
16
17#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
18pub enum ToolExecutionTarget {
19 Unspecified,
20 ClientLocal,
22 ServerAgents,
24}
25
26#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
27pub enum ToolCallApproval {
28 Unspecified,
29 Pending,
30 Approved,
31 AutoApproved,
32 Rejected,
33}
34
35#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
36pub struct ModelConfig {
37 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub temperature: Option<f32>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub top_p: Option<f32>,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub presence_penalty: Option<f32>,
43 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub frequency_penalty: Option<f32>,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub max_tokens: Option<i32>,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub reasoning_effort: Option<String>,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
52pub struct ThreadModelOverride {
53 pub model_id: Uuid,
54 pub model_config: ModelConfig,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct ToolCallOutput {
59 pub id: String,
61
62 pub is_error: bool,
63 pub output: String,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub duration_seconds: Option<i32>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
69pub enum SubagentEscalationResolution {
70 Approved,
71 Rejected {
72 #[serde(default, skip_serializing_if = "Option::is_none")]
73 reason: Option<String>,
74 },
75 ResolvedWithOutput {
76 #[serde(default)]
77 is_error: bool,
78 output: String,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
80 duration_seconds: Option<i32>,
81 },
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
85pub struct LocalSkillMetadata {
86 pub name: String,
87 pub description: String,
88 pub cwd: String,
89}
90
91trait BoolExt {
92 fn is_false(&self) -> bool;
93}
94
95impl BoolExt for bool {
96 fn is_false(&self) -> bool {
97 !*self
98 }
99}
100
101#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
103pub enum Os {
104 #[default]
106 #[serde(rename = "other")]
107 Other,
108 #[serde(rename = "linux")]
110 Linux,
111 #[serde(rename = "macos")]
113 MacOS,
114 #[serde(rename = "windows")]
116 Windows,
117}
118
119#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
121pub enum Arch {
122 #[default]
124 #[serde(rename = "other")]
125 Other,
126 #[serde(rename = "x86")]
128 X86,
129 #[serde(rename = "amd64")]
131 Amd64,
132 #[serde(rename = "aarch64")]
134 Aarch64,
135}
136
137#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
139#[serde(default)]
140pub struct ClientSystemInfo {
141 pub os: Os,
143 pub os_version: String,
145 pub arch: Arch,
147 pub cpu_cores: u16,
149 pub ram_mb: u32,
151}
152
153impl ClientSystemInfo {
154 fn is_unknown(&self) -> bool {
155 self == &Self::default()
156 }
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub enum ClientMessage {
161 HelloMath {
162 client_instance_id: String,
166 version: String,
168 min_supported_version: String,
170 #[serde(default, skip_serializing_if = "Option::is_none")]
172 resume_thread_id: Option<Uuid>,
173 #[serde(default, skip_serializing_if = "BoolExt::is_false")]
175 automagic: bool,
176 #[serde(default, skip_serializing_if = "ClientSystemInfo::is_unknown")]
180 system_info: ClientSystemInfo,
181 },
182 SendMessage {
187 request_id: Uuid,
188 thread_id: Option<Uuid>,
189 text: String,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
192 runtime_mode: Option<RuntimeMode>,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
198 model_override: Option<ThreadModelOverride>,
199 },
200 UpdateAuthToken {
202 token: String,
203 },
204 UpdateWorkspaceRoots {
208 default_root: String,
209 workspace_roots: Vec<String>,
210 },
211 UpdateLocalSkills {
213 skills: Vec<LocalSkillMetadata>,
214 },
215 RejectToolCall {
216 id: String,
217 #[serde(default, skip_serializing_if = "Option::is_none")]
218 reason: Option<String>,
219 },
220 AcceptToolCall {
221 id: String,
222 },
223 ResolveSubagentEscalation {
224 parent_message_id: Uuid,
225 subagent_run_id: Uuid,
226 escalation_id: String,
227 resolution: SubagentEscalationResolution,
228 },
229 ToolCallOutputs {
230 outputs: Vec<ToolCallOutput>,
231 },
232 CancelGeneration {
234 message_id: Uuid,
235 },
236}
237
238#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
239pub struct Usage {
240 pub input_tokens: i32,
241 pub output_tokens: i32,
242 #[serde(default)]
243 pub cache_read_input_tokens: i32,
244 #[serde(default)]
245 pub cache_creation_input_tokens: i32,
246 #[serde(default)]
247 pub cache_creation_input_tokens_5m: i32,
248 #[serde(default)]
249 pub cache_creation_input_tokens_1h: i32,
250}
251
252#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
253pub enum MessageStatus {
254 Completed,
255 WaitingForUser,
258 Failed,
259 Cancelled,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub enum ServerMessage {
264 HelloMagic {
265 version: String,
266 min_supported_version: String,
267 },
268 VersionMismatch {
269 server_version: String,
270 server_min_supported_version: String,
271 },
272 Goodbye {
273 reconnect: bool,
274 },
275 SendMessageAck {
276 request_id: Uuid,
277 thread_id: Uuid,
278 user_message_id: Uuid,
279 },
280 AuthUpdated,
281 RuntimeModeUpdated {
282 thread_id: Uuid,
283 mode: RuntimeMode,
284 #[serde(default, skip_serializing_if = "Option::is_none")]
285 changed_by_client_instance_id: Option<String>,
286 },
287 ThreadModelUpdated {
288 thread_id: Uuid,
289 #[serde(default, skip_serializing_if = "Option::is_none")]
290 model_override: Option<ThreadModelOverride>,
291 #[serde(default, skip_serializing_if = "Option::is_none")]
292 changed_by_client_instance_id: Option<String>,
293 },
294 MessageHeader {
295 message_id: Uuid,
296 thread_id: Uuid,
297 role: Role,
298 #[serde(default, skip_serializing_if = "Option::is_none")]
299 request_id: Option<Uuid>,
300 },
301 ReasoningDelta {
302 message_id: Uuid,
303 content: String,
304 },
305 TextDelta {
306 message_id: Uuid,
307 content: String,
308 },
309 ToolCallHeader {
310 message_id: Uuid,
311 tool_call_id: String,
312 name: String,
313 execution_target: ToolExecutionTarget,
314 approval: ToolCallApproval,
315 },
316 ToolCallArgumentsDelta {
317 message_id: Uuid,
318 tool_call_id: String,
319 delta: String,
320 },
321 ToolCall {
322 message_id: Uuid,
323 tool_call_id: String,
324 args: Value,
325 },
326 ToolCallResult {
327 message_id: Uuid,
328 tool_call_id: String,
329 is_error: bool,
330 output: String,
331 #[serde(default, skip_serializing_if = "Option::is_none")]
332 duration_seconds: Option<i32>,
333 },
334 ToolCallClaimed {
335 message_id: Uuid,
336 tool_call_id: String,
337 claimed_by_client_instance_id: String,
338 },
339 ToolCallApprovalUpdated {
340 message_id: Uuid,
341 tool_call_id: String,
342 approval: ToolCallApproval,
343 },
344 MessageDone {
345 message_id: Uuid,
346 #[serde(default, skip_serializing_if = "Option::is_none")]
347 usage: Option<Usage>,
348 status: MessageStatus,
349 },
350 Error {
351 #[serde(default, skip_serializing_if = "Option::is_none")]
352 request_id: Option<Uuid>,
353 #[serde(default, skip_serializing_if = "Option::is_none")]
354 message_id: Option<Uuid>,
355 code: String,
356 message: String,
357 },
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363 use serde_json::json;
364
365 fn hello_math(resume_thread_id: Option<Uuid>) -> ClientMessage {
366 ClientMessage::HelloMath {
367 client_instance_id: "client-a".to_string(),
368 version: "1.2.3".to_string(),
369 min_supported_version: "1.0.0".to_string(),
370 resume_thread_id,
371 automagic: false,
372 system_info: ClientSystemInfo::default(),
373 }
374 }
375
376 #[test]
377 fn send_message_omits_optional_updates_when_not_set() {
378 let msg = ClientMessage::SendMessage {
379 request_id: Uuid::nil(),
380 thread_id: None,
381 text: "hello".to_string(),
382 runtime_mode: None,
383 model_override: None,
384 };
385
386 let value = serde_json::to_value(msg).expect("serialize");
387 let body = value
388 .get("SendMessage")
389 .and_then(|v| v.as_object())
390 .expect("SendMessage body");
391
392 assert!(body.get("runtime_mode").is_none());
393 assert!(body.get("model_override").is_none());
394 }
395
396 #[test]
397 fn hello_math_omits_resume_thread_id_when_not_set() {
398 let msg = hello_math(None);
399
400 let value = serde_json::to_value(msg).expect("serialize");
401 let body = value
402 .get("HelloMath")
403 .and_then(|v| v.as_object())
404 .expect("HelloMath body");
405
406 assert!(body.get("resume_thread_id").is_none());
407 assert!(body.get("automagic").is_none());
408 assert!(body.get("system_info").is_none());
409 }
410
411 #[test]
412 fn hello_math_round_trip_resume_thread_id() {
413 let thread_id = Uuid::new_v4();
414 let msg = hello_math(Some(thread_id));
415
416 let value = serde_json::to_value(&msg).expect("serialize");
417 let body = value
418 .get("HelloMath")
419 .and_then(|v| v.as_object())
420 .expect("HelloMath body");
421 assert_eq!(
422 body.get("resume_thread_id"),
423 Some(&serde_json::Value::String(thread_id.to_string()))
424 );
425
426 let back: ClientMessage = serde_json::from_value(value).expect("deserialize");
427 match back {
428 ClientMessage::HelloMath {
429 resume_thread_id, ..
430 } => assert_eq!(resume_thread_id, Some(thread_id)),
431 _ => panic!("expected HelloMath"),
432 }
433 }
434
435 #[test]
436 fn hello_math_deserializes_defaults_for_new_fields() {
437 let value = json!({
438 "HelloMath": {
439 "client_instance_id": "client-a",
440 "version": "1.2.3",
441 "min_supported_version": "1.0.0"
442 }
443 });
444
445 let back: ClientMessage = serde_json::from_value(value).expect("deserialize");
446 match back {
447 ClientMessage::HelloMath {
448 automagic,
449 system_info,
450 ..
451 } => {
452 assert!(!automagic);
453 assert_eq!(system_info, ClientSystemInfo::default());
454 }
455 _ => panic!("expected HelloMath"),
456 }
457 }
458
459 #[test]
460 fn hello_math_serializes_non_default_system_info() {
461 let msg = ClientMessage::HelloMath {
462 client_instance_id: "client-a".to_string(),
463 version: "1.2.3".to_string(),
464 min_supported_version: "1.0.0".to_string(),
465 resume_thread_id: None,
466 automagic: true,
467 system_info: ClientSystemInfo {
468 os: Os::MacOS,
469 os_version: "15.5".to_string(),
470 arch: Arch::Amd64,
471 cpu_cores: 10,
472 ram_mb: 32768,
473 },
474 };
475
476 let value = serde_json::to_value(msg).expect("serialize");
477 let body = value
478 .get("HelloMath")
479 .and_then(|v| v.as_object())
480 .expect("HelloMath body");
481
482 assert_eq!(body.get("automagic"), Some(&json!(true)));
483 assert_eq!(
484 body.get("system_info"),
485 Some(&json!({
486 "os": "macos",
487 "os_version": "15.5",
488 "arch": "amd64",
489 "cpu_cores": 10,
490 "ram_mb": 32768
491 }))
492 );
493 }
494
495 #[test]
496 fn hello_math_deserializes_canonical_arch_names() {
497 let value = json!({
498 "HelloMath": {
499 "client_instance_id": "client-a",
500 "version": "1.2.3",
501 "min_supported_version": "1.0.0",
502 "system_info": {
503 "os": "linux",
504 "os_version": "6.8",
505 "arch": "amd64",
506 "cpu_cores": 8,
507 "ram_mb": 16384
508 }
509 }
510 });
511
512 let back: ClientMessage = serde_json::from_value(value).expect("deserialize");
513 match back {
514 ClientMessage::HelloMath { system_info, .. } => {
515 assert_eq!(system_info.arch, Arch::Amd64);
516 }
517 _ => panic!("expected HelloMath"),
518 }
519 }
520
521 #[test]
522 fn send_message_serializes_model_override_when_set() {
523 let msg = ClientMessage::SendMessage {
524 request_id: Uuid::nil(),
525 thread_id: Some(Uuid::nil()),
526 text: "hello".to_string(),
527 runtime_mode: Some(RuntimeMode::Plan),
528 model_override: Some(ThreadModelOverride {
529 model_id: Uuid::nil(),
530 model_config: ModelConfig::default(),
531 }),
532 };
533
534 let value = serde_json::to_value(msg).expect("serialize");
535 let body = value
536 .get("SendMessage")
537 .and_then(|v| v.as_object())
538 .expect("SendMessage body");
539
540 assert_eq!(body.get("runtime_mode"), Some(&json!("Plan")));
541 assert!(body.get("model_override").is_some());
542 }
543
544 #[test]
545 fn send_message_deserializes_model_override_states() {
546 let set_json = json!({
547 "SendMessage": {
548 "request_id": Uuid::nil(),
549 "thread_id": Uuid::nil(),
550 "text": "hello",
551 "runtime_mode": "Agent",
552 "model_override": {
553 "model_id": Uuid::nil(),
554 "model_config": {}
555 }
556 }
557 });
558 let keep_json = json!({
559 "SendMessage": {
560 "request_id": Uuid::nil(),
561 "thread_id": Uuid::nil(),
562 "text": "hello"
563 }
564 });
565
566 let set_msg: ClientMessage = serde_json::from_value(set_json).expect("deserialize set");
567 let keep_msg: ClientMessage = serde_json::from_value(keep_json).expect("deserialize keep");
568
569 match set_msg {
570 ClientMessage::SendMessage {
571 runtime_mode,
572 model_override,
573 ..
574 } => {
575 assert_eq!(runtime_mode, Some(RuntimeMode::Agent));
576 assert!(model_override.is_some());
577 }
578 _ => panic!("expected SendMessage"),
579 }
580
581 match keep_msg {
582 ClientMessage::SendMessage { model_override, .. } => {
583 assert_eq!(model_override, None);
584 }
585 _ => panic!("expected SendMessage"),
586 }
587 }
588
589 #[test]
590 fn update_local_skills_round_trip_full_and_empty() {
591 let full = ClientMessage::UpdateLocalSkills {
592 skills: vec![LocalSkillMetadata {
593 name: "Build skill".to_string(),
594 description: "Run and fix build failures".to_string(),
595 cwd: "/Users/dev/project".to_string(),
596 }],
597 };
598 let empty = ClientMessage::UpdateLocalSkills { skills: vec![] };
599
600 let full_json = serde_json::to_value(&full).expect("serialize full");
601 let empty_json = serde_json::to_value(&empty).expect("serialize empty");
602
603 let full_back: ClientMessage = serde_json::from_value(full_json).expect("deserialize full");
604 let empty_back: ClientMessage =
605 serde_json::from_value(empty_json).expect("deserialize empty");
606
607 match full_back {
608 ClientMessage::UpdateLocalSkills { skills } => {
609 assert_eq!(skills.len(), 1);
610 assert_eq!(skills[0].name, "Build skill");
611 assert_eq!(skills[0].description, "Run and fix build failures");
612 assert_eq!(skills[0].cwd, "/Users/dev/project");
613 }
614 _ => panic!("expected UpdateLocalSkills"),
615 }
616
617 match empty_back {
618 ClientMessage::UpdateLocalSkills { skills } => {
619 assert!(skills.is_empty());
620 }
621 _ => panic!("expected UpdateLocalSkills"),
622 }
623 }
624
625 #[test]
626 fn resolve_subagent_escalation_approved_round_trip() {
627 let msg = ClientMessage::ResolveSubagentEscalation {
628 parent_message_id: Uuid::nil(),
629 subagent_run_id: Uuid::nil(),
630 escalation_id: "esc-0".to_string(),
631 resolution: SubagentEscalationResolution::Approved,
632 };
633
634 let value = serde_json::to_value(&msg).expect("serialize");
635 let back: ClientMessage = serde_json::from_value(value).expect("deserialize");
636 match back {
637 ClientMessage::ResolveSubagentEscalation {
638 escalation_id,
639 resolution: SubagentEscalationResolution::Approved,
640 ..
641 } => assert_eq!(escalation_id, "esc-0"),
642 _ => panic!("expected ResolveSubagentEscalation::Approved"),
643 }
644 }
645
646 #[test]
647 fn resolve_subagent_escalation_rejected_round_trip() {
648 let msg = ClientMessage::ResolveSubagentEscalation {
649 parent_message_id: Uuid::nil(),
650 subagent_run_id: Uuid::nil(),
651 escalation_id: "esc-1".to_string(),
652 resolution: SubagentEscalationResolution::Rejected {
653 reason: Some("not now".to_string()),
654 },
655 };
656
657 let value = serde_json::to_value(&msg).expect("serialize");
658 let body = value
659 .get("ResolveSubagentEscalation")
660 .and_then(|v| v.as_object())
661 .expect("ResolveSubagentEscalation body");
662 assert_eq!(body.get("escalation_id"), Some(&json!("esc-1")));
663
664 let back: ClientMessage = serde_json::from_value(value).expect("deserialize");
665 match back {
666 ClientMessage::ResolveSubagentEscalation {
667 resolution:
668 SubagentEscalationResolution::Rejected {
669 reason: Some(reason),
670 },
671 ..
672 } => assert_eq!(reason, "not now"),
673 _ => panic!("expected ResolveSubagentEscalation::Rejected"),
674 }
675 }
676
677 #[test]
678 fn resolve_subagent_escalation_resolved_with_output_round_trip() {
679 let msg = ClientMessage::ResolveSubagentEscalation {
680 parent_message_id: Uuid::nil(),
681 subagent_run_id: Uuid::nil(),
682 escalation_id: "esc-2".to_string(),
683 resolution: SubagentEscalationResolution::ResolvedWithOutput {
684 is_error: false,
685 output: "ok".to_string(),
686 duration_seconds: Some(3),
687 },
688 };
689
690 let value = serde_json::to_value(&msg).expect("serialize");
691 let back: ClientMessage = serde_json::from_value(value).expect("deserialize");
692 match back {
693 ClientMessage::ResolveSubagentEscalation {
694 escalation_id,
695 resolution:
696 SubagentEscalationResolution::ResolvedWithOutput {
697 is_error,
698 output,
699 duration_seconds,
700 },
701 ..
702 } => {
703 assert_eq!(escalation_id, "esc-2");
704 assert!(!is_error);
705 assert_eq!(output, "ok");
706 assert_eq!(duration_seconds, Some(3));
707 }
708 _ => panic!("expected ResolveSubagentEscalation::ResolvedWithOutput"),
709 }
710 }
711}