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 Skill {
86 pub name: String,
87 pub description: String,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
91pub struct Rule {
92 pub name: String,
93 pub description: String,
94 #[serde(default)]
95 pub always_apply: bool,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
99pub struct WorkspaceRoot {
100 pub cwd: String,
101 #[serde(default)]
102 pub agents_md: String,
103 #[serde(default)]
104 pub rules: Vec<Rule>,
105 #[serde(default)]
106 pub skills: Vec<Skill>,
107}
108
109trait BoolExt {
110 fn is_false(&self) -> bool;
111}
112
113impl BoolExt for bool {
114 fn is_false(&self) -> bool {
115 !*self
116 }
117}
118
119#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
121pub enum Os {
122 #[default]
124 #[serde(rename = "other")]
125 Other,
126 #[serde(rename = "linux")]
128 Linux,
129 #[serde(rename = "macos")]
131 MacOS,
132 #[serde(rename = "windows")]
134 Windows,
135}
136
137#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
139pub enum Arch {
140 #[default]
142 #[serde(rename = "other")]
143 Other,
144 #[serde(rename = "x86")]
146 X86,
147 #[serde(rename = "amd64")]
149 Amd64,
150 #[serde(rename = "aarch64")]
152 Aarch64,
153}
154
155#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
157#[serde(default)]
158pub struct ClientSystemInfo {
159 pub os: Os,
161 pub os_version: String,
163 pub arch: Arch,
165 pub cpu_cores: u16,
167 pub ram_mb: u32,
169}
170
171impl ClientSystemInfo {
172 fn is_unknown(&self) -> bool {
173 self == &Self::default()
174 }
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub enum ClientMessage {
179 HelloMath {
180 client_instance_id: String,
184 version: String,
186 min_supported_version: String,
188 #[serde(default, skip_serializing_if = "Option::is_none")]
190 resume_thread_id: Option<Uuid>,
191 #[serde(default, skip_serializing_if = "BoolExt::is_false")]
193 automagic: bool,
194 #[serde(default, skip_serializing_if = "ClientSystemInfo::is_unknown")]
198 system_info: ClientSystemInfo,
199 },
200 SendMessage {
205 request_id: Uuid,
206 thread_id: Option<Uuid>,
207 text: String,
208 #[serde(default, skip_serializing_if = "Option::is_none")]
210 runtime_mode: Option<RuntimeMode>,
211 #[serde(default, skip_serializing_if = "Option::is_none")]
216 model_override: Option<ThreadModelOverride>,
217 },
218 UpdateAuthToken {
220 token: String,
221 },
222 UpdateWorkspaceRoots {
226 workspace_roots: Vec<WorkspaceRoot>,
227 },
228 RejectToolCall {
229 id: String,
230 #[serde(default, skip_serializing_if = "Option::is_none")]
231 reason: Option<String>,
232 },
233 AcceptToolCall {
234 id: String,
235 },
236 ResolveSubagentEscalation {
237 parent_message_id: Uuid,
238 subagent_run_id: Uuid,
239 escalation_id: String,
240 resolution: SubagentEscalationResolution,
241 },
242 ToolCallOutputs {
243 outputs: Vec<ToolCallOutput>,
244 },
245 CancelGeneration {
247 message_id: Uuid,
248 },
249}
250
251#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
252pub struct Usage {
253 pub input_tokens: i32,
254 pub output_tokens: i32,
255 #[serde(default)]
256 pub cache_read_input_tokens: i32,
257 #[serde(default)]
258 pub cache_creation_input_tokens: i32,
259 #[serde(default)]
260 pub cache_creation_input_tokens_5m: i32,
261 #[serde(default)]
262 pub cache_creation_input_tokens_1h: i32,
263}
264
265#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
266pub enum MessageStatus {
267 Completed,
268 WaitingForUser,
271 Failed,
272 Cancelled,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub enum ServerMessage {
277 HelloMagic {
278 version: String,
279 min_supported_version: String,
280 },
281 VersionMismatch {
282 server_version: String,
283 server_min_supported_version: String,
284 },
285 Goodbye {
286 reconnect: bool,
287 },
288 SendMessageAck {
289 request_id: Uuid,
290 thread_id: Uuid,
291 user_message_id: Uuid,
292 },
293 AuthUpdated,
294 RuntimeModeUpdated {
295 thread_id: Uuid,
296 mode: RuntimeMode,
297 #[serde(default, skip_serializing_if = "Option::is_none")]
298 changed_by_client_instance_id: Option<String>,
299 },
300 ThreadModelUpdated {
301 thread_id: Uuid,
302 #[serde(default, skip_serializing_if = "Option::is_none")]
303 model_override: Option<ThreadModelOverride>,
304 #[serde(default, skip_serializing_if = "Option::is_none")]
305 changed_by_client_instance_id: Option<String>,
306 },
307 MessageHeader {
308 message_id: Uuid,
309 thread_id: Uuid,
310 role: Role,
311 #[serde(default, skip_serializing_if = "Option::is_none")]
312 request_id: Option<Uuid>,
313 },
314 ReasoningDelta {
315 message_id: Uuid,
316 content: String,
317 },
318 TextDelta {
319 message_id: Uuid,
320 content: String,
321 },
322 ToolCallHeader {
323 message_id: Uuid,
324 tool_call_id: String,
325 name: String,
326 execution_target: ToolExecutionTarget,
327 approval: ToolCallApproval,
328 },
329 ToolCallArgumentsDelta {
330 message_id: Uuid,
331 tool_call_id: String,
332 delta: String,
333 },
334 ToolCall {
335 message_id: Uuid,
336 tool_call_id: String,
337 args: Value,
338 },
339 ToolCallResult {
340 message_id: Uuid,
341 tool_call_id: String,
342 is_error: bool,
343 output: String,
344 #[serde(default, skip_serializing_if = "Option::is_none")]
345 duration_seconds: Option<i32>,
346 },
347 ToolCallClaimed {
348 message_id: Uuid,
349 tool_call_id: String,
350 claimed_by_client_instance_id: String,
351 },
352 ToolCallApprovalUpdated {
353 message_id: Uuid,
354 tool_call_id: String,
355 approval: ToolCallApproval,
356 },
357 MessageDone {
358 message_id: Uuid,
359 #[serde(default, skip_serializing_if = "Option::is_none")]
360 usage: Option<Usage>,
361 status: MessageStatus,
362 },
363 Error {
364 #[serde(default, skip_serializing_if = "Option::is_none")]
365 request_id: Option<Uuid>,
366 #[serde(default, skip_serializing_if = "Option::is_none")]
367 message_id: Option<Uuid>,
368 code: String,
369 message: String,
370 },
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376 use serde_json::json;
377
378 fn hello_math(resume_thread_id: Option<Uuid>) -> ClientMessage {
379 ClientMessage::HelloMath {
380 client_instance_id: "client-a".to_string(),
381 version: "1.2.3".to_string(),
382 min_supported_version: "1.0.0".to_string(),
383 resume_thread_id,
384 automagic: false,
385 system_info: ClientSystemInfo::default(),
386 }
387 }
388
389 #[test]
390 fn send_message_omits_optional_updates_when_not_set() {
391 let msg = ClientMessage::SendMessage {
392 request_id: Uuid::nil(),
393 thread_id: None,
394 text: "hello".to_string(),
395 runtime_mode: None,
396 model_override: None,
397 };
398
399 let value = serde_json::to_value(msg).expect("serialize");
400 let body = value
401 .get("SendMessage")
402 .and_then(|v| v.as_object())
403 .expect("SendMessage body");
404
405 assert!(body.get("runtime_mode").is_none());
406 assert!(body.get("model_override").is_none());
407 }
408
409 #[test]
410 fn hello_math_omits_resume_thread_id_when_not_set() {
411 let msg = hello_math(None);
412
413 let value = serde_json::to_value(msg).expect("serialize");
414 let body = value
415 .get("HelloMath")
416 .and_then(|v| v.as_object())
417 .expect("HelloMath body");
418
419 assert!(body.get("resume_thread_id").is_none());
420 assert!(body.get("automagic").is_none());
421 assert!(body.get("system_info").is_none());
422 }
423
424 #[test]
425 fn hello_math_round_trip_resume_thread_id() {
426 let thread_id = Uuid::new_v4();
427 let msg = hello_math(Some(thread_id));
428
429 let value = serde_json::to_value(&msg).expect("serialize");
430 let body = value
431 .get("HelloMath")
432 .and_then(|v| v.as_object())
433 .expect("HelloMath body");
434 assert_eq!(
435 body.get("resume_thread_id"),
436 Some(&serde_json::Value::String(thread_id.to_string()))
437 );
438
439 let back: ClientMessage = serde_json::from_value(value).expect("deserialize");
440 match back {
441 ClientMessage::HelloMath {
442 resume_thread_id, ..
443 } => assert_eq!(resume_thread_id, Some(thread_id)),
444 _ => panic!("expected HelloMath"),
445 }
446 }
447
448 #[test]
449 fn hello_math_deserializes_defaults_for_new_fields() {
450 let value = json!({
451 "HelloMath": {
452 "client_instance_id": "client-a",
453 "version": "1.2.3",
454 "min_supported_version": "1.0.0"
455 }
456 });
457
458 let back: ClientMessage = serde_json::from_value(value).expect("deserialize");
459 match back {
460 ClientMessage::HelloMath {
461 automagic,
462 system_info,
463 ..
464 } => {
465 assert!(!automagic);
466 assert_eq!(system_info, ClientSystemInfo::default());
467 }
468 _ => panic!("expected HelloMath"),
469 }
470 }
471
472 #[test]
473 fn hello_math_serializes_non_default_system_info() {
474 let msg = ClientMessage::HelloMath {
475 client_instance_id: "client-a".to_string(),
476 version: "1.2.3".to_string(),
477 min_supported_version: "1.0.0".to_string(),
478 resume_thread_id: None,
479 automagic: true,
480 system_info: ClientSystemInfo {
481 os: Os::MacOS,
482 os_version: "15.5".to_string(),
483 arch: Arch::Amd64,
484 cpu_cores: 10,
485 ram_mb: 32768,
486 },
487 };
488
489 let value = serde_json::to_value(msg).expect("serialize");
490 let body = value
491 .get("HelloMath")
492 .and_then(|v| v.as_object())
493 .expect("HelloMath body");
494
495 assert_eq!(body.get("automagic"), Some(&json!(true)));
496 assert_eq!(
497 body.get("system_info"),
498 Some(&json!({
499 "os": "macos",
500 "os_version": "15.5",
501 "arch": "amd64",
502 "cpu_cores": 10,
503 "ram_mb": 32768
504 }))
505 );
506 }
507
508 #[test]
509 fn hello_math_deserializes_canonical_arch_names() {
510 let value = json!({
511 "HelloMath": {
512 "client_instance_id": "client-a",
513 "version": "1.2.3",
514 "min_supported_version": "1.0.0",
515 "system_info": {
516 "os": "linux",
517 "os_version": "6.8",
518 "arch": "amd64",
519 "cpu_cores": 8,
520 "ram_mb": 16384
521 }
522 }
523 });
524
525 let back: ClientMessage = serde_json::from_value(value).expect("deserialize");
526 match back {
527 ClientMessage::HelloMath { system_info, .. } => {
528 assert_eq!(system_info.arch, Arch::Amd64);
529 }
530 _ => panic!("expected HelloMath"),
531 }
532 }
533
534 #[test]
535 fn send_message_serializes_model_override_when_set() {
536 let msg = ClientMessage::SendMessage {
537 request_id: Uuid::nil(),
538 thread_id: Some(Uuid::nil()),
539 text: "hello".to_string(),
540 runtime_mode: Some(RuntimeMode::Plan),
541 model_override: Some(ThreadModelOverride {
542 model_id: Uuid::nil(),
543 model_config: ModelConfig::default(),
544 }),
545 };
546
547 let value = serde_json::to_value(msg).expect("serialize");
548 let body = value
549 .get("SendMessage")
550 .and_then(|v| v.as_object())
551 .expect("SendMessage body");
552
553 assert_eq!(body.get("runtime_mode"), Some(&json!("Plan")));
554 assert!(body.get("model_override").is_some());
555 }
556
557 #[test]
558 fn send_message_deserializes_model_override_states() {
559 let set_json = json!({
560 "SendMessage": {
561 "request_id": Uuid::nil(),
562 "thread_id": Uuid::nil(),
563 "text": "hello",
564 "runtime_mode": "Agent",
565 "model_override": {
566 "model_id": Uuid::nil(),
567 "model_config": {}
568 }
569 }
570 });
571 let keep_json = json!({
572 "SendMessage": {
573 "request_id": Uuid::nil(),
574 "thread_id": Uuid::nil(),
575 "text": "hello"
576 }
577 });
578
579 let set_msg: ClientMessage = serde_json::from_value(set_json).expect("deserialize set");
580 let keep_msg: ClientMessage = serde_json::from_value(keep_json).expect("deserialize keep");
581
582 match set_msg {
583 ClientMessage::SendMessage {
584 runtime_mode,
585 model_override,
586 ..
587 } => {
588 assert_eq!(runtime_mode, Some(RuntimeMode::Agent));
589 assert!(model_override.is_some());
590 }
591 _ => panic!("expected SendMessage"),
592 }
593
594 match keep_msg {
595 ClientMessage::SendMessage { model_override, .. } => {
596 assert_eq!(model_override, None);
597 }
598 _ => panic!("expected SendMessage"),
599 }
600 }
601
602 #[test]
603 fn update_workspace_roots_round_trip_full_and_empty() {
604 let full = ClientMessage::UpdateWorkspaceRoots {
605 workspace_roots: vec![WorkspaceRoot {
606 cwd: "/Users/dev/project".to_string(),
607 agents_md: "Follow the repository rules first.\nThen run tests.".to_string(),
608 rules: vec![Rule {
609 name: "Test after changes".to_string(),
610 description: "Run the relevant tests before finishing.".to_string(),
611 always_apply: true,
612 }],
613 skills: vec![Skill {
614 name: "Build skill".to_string(),
615 description: "Run and fix build failures".to_string(),
616 }],
617 }],
618 };
619 let empty = ClientMessage::UpdateWorkspaceRoots {
620 workspace_roots: vec![],
621 };
622
623 let full_json = serde_json::to_value(&full).expect("serialize full");
624 let empty_json = serde_json::to_value(&empty).expect("serialize empty");
625
626 let full_back: ClientMessage = serde_json::from_value(full_json).expect("deserialize full");
627 let empty_back: ClientMessage =
628 serde_json::from_value(empty_json).expect("deserialize empty");
629
630 match full_back {
631 ClientMessage::UpdateWorkspaceRoots { workspace_roots } => {
632 assert_eq!(workspace_roots.len(), 1);
633 assert_eq!(workspace_roots[0].cwd, "/Users/dev/project");
634 assert_eq!(
635 workspace_roots[0].agents_md,
636 "Follow the repository rules first.\nThen run tests."
637 );
638 assert_eq!(workspace_roots[0].rules.len(), 1);
639 assert_eq!(workspace_roots[0].rules[0].name, "Test after changes");
640 assert_eq!(
641 workspace_roots[0].rules[0].description,
642 "Run the relevant tests before finishing."
643 );
644 assert!(workspace_roots[0].rules[0].always_apply);
645 assert_eq!(workspace_roots[0].skills.len(), 1);
646 assert_eq!(workspace_roots[0].skills[0].name, "Build skill");
647 assert_eq!(
648 workspace_roots[0].skills[0].description,
649 "Run and fix build failures"
650 );
651 }
652 _ => panic!("expected UpdateWorkspaceRoots"),
653 }
654
655 match empty_back {
656 ClientMessage::UpdateWorkspaceRoots { workspace_roots } => {
657 assert!(workspace_roots.is_empty());
658 }
659 _ => panic!("expected UpdateWorkspaceRoots"),
660 }
661 }
662
663 #[test]
664 fn update_workspace_roots_defaults_missing_nested_fields() {
665 let json = json!({
666 "UpdateWorkspaceRoots": {
667 "workspace_roots": [{
668 "cwd": "/Users/dev/project"
669 }]
670 }
671 });
672
673 let back: ClientMessage = serde_json::from_value(json).expect("deserialize");
674
675 match back {
676 ClientMessage::UpdateWorkspaceRoots { workspace_roots } => {
677 assert_eq!(workspace_roots.len(), 1);
678 assert_eq!(workspace_roots[0].cwd, "/Users/dev/project");
679 assert!(workspace_roots[0].agents_md.is_empty());
680 assert!(workspace_roots[0].rules.is_empty());
681 assert!(workspace_roots[0].skills.is_empty());
682 }
683 _ => panic!("expected UpdateWorkspaceRoots"),
684 }
685 }
686
687 #[test]
688 fn resolve_subagent_escalation_approved_round_trip() {
689 let msg = ClientMessage::ResolveSubagentEscalation {
690 parent_message_id: Uuid::nil(),
691 subagent_run_id: Uuid::nil(),
692 escalation_id: "esc-0".to_string(),
693 resolution: SubagentEscalationResolution::Approved,
694 };
695
696 let value = serde_json::to_value(&msg).expect("serialize");
697 let back: ClientMessage = serde_json::from_value(value).expect("deserialize");
698 match back {
699 ClientMessage::ResolveSubagentEscalation {
700 escalation_id,
701 resolution: SubagentEscalationResolution::Approved,
702 ..
703 } => assert_eq!(escalation_id, "esc-0"),
704 _ => panic!("expected ResolveSubagentEscalation::Approved"),
705 }
706 }
707
708 #[test]
709 fn resolve_subagent_escalation_rejected_round_trip() {
710 let msg = ClientMessage::ResolveSubagentEscalation {
711 parent_message_id: Uuid::nil(),
712 subagent_run_id: Uuid::nil(),
713 escalation_id: "esc-1".to_string(),
714 resolution: SubagentEscalationResolution::Rejected {
715 reason: Some("not now".to_string()),
716 },
717 };
718
719 let value = serde_json::to_value(&msg).expect("serialize");
720 let body = value
721 .get("ResolveSubagentEscalation")
722 .and_then(|v| v.as_object())
723 .expect("ResolveSubagentEscalation body");
724 assert_eq!(body.get("escalation_id"), Some(&json!("esc-1")));
725
726 let back: ClientMessage = serde_json::from_value(value).expect("deserialize");
727 match back {
728 ClientMessage::ResolveSubagentEscalation {
729 resolution:
730 SubagentEscalationResolution::Rejected {
731 reason: Some(reason),
732 },
733 ..
734 } => assert_eq!(reason, "not now"),
735 _ => panic!("expected ResolveSubagentEscalation::Rejected"),
736 }
737 }
738
739 #[test]
740 fn resolve_subagent_escalation_resolved_with_output_round_trip() {
741 let msg = ClientMessage::ResolveSubagentEscalation {
742 parent_message_id: Uuid::nil(),
743 subagent_run_id: Uuid::nil(),
744 escalation_id: "esc-2".to_string(),
745 resolution: SubagentEscalationResolution::ResolvedWithOutput {
746 is_error: false,
747 output: "ok".to_string(),
748 duration_seconds: Some(3),
749 },
750 };
751
752 let value = serde_json::to_value(&msg).expect("serialize");
753 let back: ClientMessage = serde_json::from_value(value).expect("deserialize");
754 match back {
755 ClientMessage::ResolveSubagentEscalation {
756 escalation_id,
757 resolution:
758 SubagentEscalationResolution::ResolvedWithOutput {
759 is_error,
760 output,
761 duration_seconds,
762 },
763 ..
764 } => {
765 assert_eq!(escalation_id, "esc-2");
766 assert!(!is_error);
767 assert_eq!(output, "ok");
768 assert_eq!(duration_seconds, Some(3));
769 }
770 _ => panic!("expected ResolveSubagentEscalation::ResolvedWithOutput"),
771 }
772 }
773}