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 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub allow_long_context: Option<bool>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
55pub struct ThreadModelOverride {
56 pub model_id: Uuid,
57 pub model_config: ModelConfig,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct ToolCallOutput {
62 pub id: String,
64
65 pub is_error: bool,
66 pub output: String,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub duration_seconds: Option<i32>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
72pub enum SubagentEscalationResolution {
73 Approved,
74 Rejected {
75 #[serde(default, skip_serializing_if = "Option::is_none")]
76 reason: Option<String>,
77 },
78 ResolvedWithOutput {
79 #[serde(default)]
80 is_error: bool,
81 output: String,
82 #[serde(default, skip_serializing_if = "Option::is_none")]
83 duration_seconds: Option<i32>,
84 },
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
88pub struct Skill {
89 pub name: String,
90 pub description: String,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
94pub struct Rule {
95 pub name: String,
96 pub description: String,
97 pub text: Option<String>,
98 #[serde(default)]
99 pub always_apply: bool,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
103pub struct WorkingDirectory {
104 pub path: String,
105 #[serde(default)]
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub git_branch: Option<String>,
108 #[serde(default)]
109 pub agents_md: String,
110 #[serde(default)]
111 pub rules: Vec<Rule>,
112 #[serde(default)]
113 pub skills: Vec<Skill>,
114}
115
116#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
117#[serde(rename_all = "snake_case")]
118pub enum BackgroundShellStatus {
119 Running,
120 Exited,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
124pub struct BackgroundShellSnapshot {
125 pub shell_id: String,
126 pub command: String,
127 pub status: BackgroundShellStatus,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub exit_code: Option<i32>,
130 pub log_lines: u64,
131 pub duration_seconds: u64,
132}
133
134trait BoolExt {
135 fn is_false(&self) -> bool;
136}
137
138impl BoolExt for bool {
139 fn is_false(&self) -> bool {
140 !*self
141 }
142}
143
144#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
146pub enum Os {
147 #[default]
149 #[serde(rename = "other")]
150 Other,
151 #[serde(rename = "linux")]
153 Linux,
154 #[serde(rename = "macos")]
156 MacOS,
157 #[serde(rename = "windows")]
159 Windows,
160}
161
162#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
164pub enum Arch {
165 #[default]
167 #[serde(rename = "other")]
168 Other,
169 #[serde(rename = "x86")]
171 X86,
172 #[serde(rename = "amd64")]
174 Amd64,
175 #[serde(rename = "aarch64")]
177 Aarch64,
178}
179
180#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
182#[serde(default)]
183pub struct ClientSystemInfo {
184 pub os: Os,
186 pub os_version: String,
188 pub arch: Arch,
190 pub cpu_cores: u16,
192 pub ram_mb: u32,
194}
195
196impl ClientSystemInfo {
197 fn is_unknown(&self) -> bool {
198 self == &Self::default()
199 }
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub enum ClientMessage {
204 HelloMath {
205 client_instance_id: String,
209 version: String,
211 min_supported_version: String,
213 #[serde(default, skip_serializing_if = "Option::is_none")]
215 resume_thread_id: Option<Uuid>,
216 #[serde(default, skip_serializing_if = "BoolExt::is_false")]
218 automagic: bool,
219 #[serde(default, skip_serializing_if = "ClientSystemInfo::is_unknown")]
223 system_info: ClientSystemInfo,
224 },
225 SendMessage {
230 request_id: Uuid,
231 thread_id: Option<Uuid>,
232 text: String,
233 #[serde(default, skip_serializing_if = "Option::is_none")]
235 runtime_mode: Option<RuntimeMode>,
236 #[serde(default, skip_serializing_if = "Option::is_none")]
241 model_override: Option<ThreadModelOverride>,
242 },
243 UpdateAuthToken {
245 token: String,
246 },
247 UpdateWorkingDirectories {
251 working_directories: Vec<WorkingDirectory>,
252 },
253 UpdateBackgroundShells {
255 shells: Vec<BackgroundShellSnapshot>,
256 },
257 RejectToolCall {
258 id: String,
259 #[serde(default, skip_serializing_if = "Option::is_none")]
260 reason: Option<String>,
261 },
262 AcceptToolCall {
263 id: String,
264 },
265 ResolveSubagentEscalation {
266 parent_message_id: Uuid,
267 subagent_run_id: Uuid,
268 escalation_id: String,
269 resolution: SubagentEscalationResolution,
270 },
271 ToolCallOutputs {
272 outputs: Vec<ToolCallOutput>,
273 },
274 CancelGeneration {
276 message_id: Uuid,
277 },
278}
279
280#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
281pub struct Usage {
282 pub input_tokens: i32,
283 pub output_tokens: i32,
284 #[serde(default)]
285 pub cache_read_input_tokens: i32,
286 #[serde(default)]
287 pub cache_creation_input_tokens: i32,
288 #[serde(default)]
289 pub cache_creation_input_tokens_5m: i32,
290 #[serde(default)]
291 pub cache_creation_input_tokens_1h: i32,
292}
293
294#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
295pub enum MessageStatus {
296 Completed,
297 WaitingForUser,
300 Failed,
301 Cancelled,
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub enum ServerMessage {
306 HelloMagic {
307 version: String,
308 min_supported_version: String,
309 },
310 VersionMismatch {
311 server_version: String,
312 server_min_supported_version: String,
313 },
314 Goodbye {
315 reconnect: bool,
316 },
317 SendMessageAck {
318 request_id: Uuid,
319 thread_id: Uuid,
320 user_message_id: Uuid,
321 },
322 AuthUpdated,
323 RuntimeModeUpdated {
324 thread_id: Uuid,
325 mode: RuntimeMode,
326 #[serde(default, skip_serializing_if = "Option::is_none")]
327 changed_by_client_instance_id: Option<String>,
328 },
329 ThreadModelUpdated {
330 thread_id: Uuid,
331 #[serde(default, skip_serializing_if = "Option::is_none")]
332 model_override: Option<ThreadModelOverride>,
333 #[serde(default, skip_serializing_if = "Option::is_none")]
334 changed_by_client_instance_id: Option<String>,
335 },
336 MessageHeader {
337 message_id: Uuid,
338 thread_id: Uuid,
339 role: Role,
340 #[serde(default, skip_serializing_if = "Option::is_none")]
341 request_id: Option<Uuid>,
342 },
343 ReasoningDelta {
344 message_id: Uuid,
345 content: String,
346 },
347 TextDelta {
348 message_id: Uuid,
349 content: String,
350 },
351 ToolCallHeader {
352 message_id: Uuid,
353 tool_call_id: String,
354 name: String,
355 execution_target: ToolExecutionTarget,
356 approval: ToolCallApproval,
357 },
358 ToolCallArgumentsDelta {
359 message_id: Uuid,
360 tool_call_id: String,
361 delta: String,
362 },
363 ToolCall {
364 message_id: Uuid,
365 tool_call_id: String,
366 args: Value,
367 },
368 ToolCallResult {
369 message_id: Uuid,
370 tool_call_id: String,
371 is_error: bool,
372 output: String,
373 #[serde(default, skip_serializing_if = "Option::is_none")]
374 duration_seconds: Option<i32>,
375 },
376 ToolCallClaimed {
377 message_id: Uuid,
378 tool_call_id: String,
379 claimed_by_client_instance_id: String,
380 },
381 ToolCallApprovalUpdated {
382 message_id: Uuid,
383 tool_call_id: String,
384 approval: ToolCallApproval,
385 },
386 MessageDone {
387 message_id: Uuid,
388 #[serde(default, skip_serializing_if = "Option::is_none")]
389 usage: Option<Usage>,
390 status: MessageStatus,
391 },
392 Error {
393 #[serde(default, skip_serializing_if = "Option::is_none")]
394 request_id: Option<Uuid>,
395 #[serde(default, skip_serializing_if = "Option::is_none")]
396 message_id: Option<Uuid>,
397 code: String,
398 message: String,
399 },
400}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405 use serde_json::json;
406
407 fn background_shell_snapshot(
408 shell_id: &str,
409 status: BackgroundShellStatus,
410 ) -> BackgroundShellSnapshot {
411 BackgroundShellSnapshot {
412 shell_id: shell_id.to_string(),
413 command: "sleep 30".to_string(),
414 status,
415 exit_code: (status == BackgroundShellStatus::Exited).then_some(0),
416 log_lines: 12,
417 duration_seconds: 18,
418 }
419 }
420
421 fn hello_math(resume_thread_id: Option<Uuid>) -> ClientMessage {
422 ClientMessage::HelloMath {
423 client_instance_id: "client-a".to_string(),
424 version: "1.2.3".to_string(),
425 min_supported_version: "1.0.0".to_string(),
426 resume_thread_id,
427 automagic: false,
428 system_info: ClientSystemInfo::default(),
429 }
430 }
431
432 #[test]
433 fn model_config_omits_allow_long_context_when_not_set() {
434 let config = ModelConfig::default();
435
436 let value = serde_json::to_value(config).expect("serialize");
437 let body = value.as_object().expect("model config body");
438
439 assert!(body.get("allow_long_context").is_none());
440 }
441
442 #[test]
443 fn model_config_round_trips_allow_long_context_when_set() {
444 for expected in [true, false] {
445 let config = ModelConfig {
446 allow_long_context: Some(expected),
447 ..ModelConfig::default()
448 };
449
450 let value = serde_json::to_value(&config).expect("serialize");
451 let body = value.as_object().expect("model config body");
452 assert_eq!(body.get("allow_long_context"), Some(&json!(expected)));
453
454 let back: ModelConfig = serde_json::from_value(value).expect("deserialize");
455 assert_eq!(back.allow_long_context, Some(expected));
456 }
457 }
458
459 #[test]
460 fn model_config_defaults_allow_long_context_to_none_when_missing() {
461 let back: ModelConfig = serde_json::from_value(json!({
462 "temperature": 0.3
463 }))
464 .expect("deserialize");
465
466 assert_eq!(back.temperature, Some(0.3));
467 assert_eq!(back.allow_long_context, None);
468 }
469
470 #[test]
471 fn send_message_omits_optional_updates_when_not_set() {
472 let msg = ClientMessage::SendMessage {
473 request_id: Uuid::nil(),
474 thread_id: None,
475 text: "hello".to_string(),
476 runtime_mode: None,
477 model_override: None,
478 };
479
480 let value = serde_json::to_value(msg).expect("serialize");
481 let body = value
482 .get("SendMessage")
483 .and_then(|v| v.as_object())
484 .expect("SendMessage body");
485
486 assert!(body.get("runtime_mode").is_none());
487 assert!(body.get("model_override").is_none());
488 }
489
490 #[test]
491 fn hello_math_omits_resume_thread_id_when_not_set() {
492 let msg = hello_math(None);
493
494 let value = serde_json::to_value(msg).expect("serialize");
495 let body = value
496 .get("HelloMath")
497 .and_then(|v| v.as_object())
498 .expect("HelloMath body");
499
500 assert!(body.get("resume_thread_id").is_none());
501 assert!(body.get("automagic").is_none());
502 assert!(body.get("system_info").is_none());
503 }
504
505 #[test]
506 fn hello_math_round_trip_resume_thread_id() {
507 let thread_id = Uuid::new_v4();
508 let msg = hello_math(Some(thread_id));
509
510 let value = serde_json::to_value(&msg).expect("serialize");
511 let body = value
512 .get("HelloMath")
513 .and_then(|v| v.as_object())
514 .expect("HelloMath body");
515 assert_eq!(
516 body.get("resume_thread_id"),
517 Some(&serde_json::Value::String(thread_id.to_string()))
518 );
519
520 let back: ClientMessage = serde_json::from_value(value).expect("deserialize");
521 match back {
522 ClientMessage::HelloMath {
523 resume_thread_id, ..
524 } => assert_eq!(resume_thread_id, Some(thread_id)),
525 _ => panic!("expected HelloMath"),
526 }
527 }
528
529 #[test]
530 fn hello_math_deserializes_defaults_for_new_fields() {
531 let value = json!({
532 "HelloMath": {
533 "client_instance_id": "client-a",
534 "version": "1.2.3",
535 "min_supported_version": "1.0.0"
536 }
537 });
538
539 let back: ClientMessage = serde_json::from_value(value).expect("deserialize");
540 match back {
541 ClientMessage::HelloMath {
542 automagic,
543 system_info,
544 ..
545 } => {
546 assert!(!automagic);
547 assert_eq!(system_info, ClientSystemInfo::default());
548 }
549 _ => panic!("expected HelloMath"),
550 }
551 }
552
553 #[test]
554 fn hello_math_serializes_non_default_system_info() {
555 let msg = ClientMessage::HelloMath {
556 client_instance_id: "client-a".to_string(),
557 version: "1.2.3".to_string(),
558 min_supported_version: "1.0.0".to_string(),
559 resume_thread_id: None,
560 automagic: true,
561 system_info: ClientSystemInfo {
562 os: Os::MacOS,
563 os_version: "15.5".to_string(),
564 arch: Arch::Amd64,
565 cpu_cores: 10,
566 ram_mb: 32768,
567 },
568 };
569
570 let value = serde_json::to_value(msg).expect("serialize");
571 let body = value
572 .get("HelloMath")
573 .and_then(|v| v.as_object())
574 .expect("HelloMath body");
575
576 assert_eq!(body.get("automagic"), Some(&json!(true)));
577 assert_eq!(
578 body.get("system_info"),
579 Some(&json!({
580 "os": "macos",
581 "os_version": "15.5",
582 "arch": "amd64",
583 "cpu_cores": 10,
584 "ram_mb": 32768
585 }))
586 );
587 }
588
589 #[test]
590 fn hello_math_deserializes_canonical_arch_names() {
591 let value = json!({
592 "HelloMath": {
593 "client_instance_id": "client-a",
594 "version": "1.2.3",
595 "min_supported_version": "1.0.0",
596 "system_info": {
597 "os": "linux",
598 "os_version": "6.8",
599 "arch": "amd64",
600 "cpu_cores": 8,
601 "ram_mb": 16384
602 }
603 }
604 });
605
606 let back: ClientMessage = serde_json::from_value(value).expect("deserialize");
607 match back {
608 ClientMessage::HelloMath { system_info, .. } => {
609 assert_eq!(system_info.arch, Arch::Amd64);
610 }
611 _ => panic!("expected HelloMath"),
612 }
613 }
614
615 #[test]
616 fn send_message_serializes_model_override_when_set() {
617 let msg = ClientMessage::SendMessage {
618 request_id: Uuid::nil(),
619 thread_id: Some(Uuid::nil()),
620 text: "hello".to_string(),
621 runtime_mode: Some(RuntimeMode::Plan),
622 model_override: Some(ThreadModelOverride {
623 model_id: Uuid::nil(),
624 model_config: ModelConfig::default(),
625 }),
626 };
627
628 let value = serde_json::to_value(msg).expect("serialize");
629 let body = value
630 .get("SendMessage")
631 .and_then(|v| v.as_object())
632 .expect("SendMessage body");
633
634 assert_eq!(body.get("runtime_mode"), Some(&json!("Plan")));
635 assert!(body.get("model_override").is_some());
636 }
637
638 #[test]
639 fn send_message_deserializes_model_override_states() {
640 let set_json = json!({
641 "SendMessage": {
642 "request_id": Uuid::nil(),
643 "thread_id": Uuid::nil(),
644 "text": "hello",
645 "runtime_mode": "Agent",
646 "model_override": {
647 "model_id": Uuid::nil(),
648 "model_config": {}
649 }
650 }
651 });
652 let keep_json = json!({
653 "SendMessage": {
654 "request_id": Uuid::nil(),
655 "thread_id": Uuid::nil(),
656 "text": "hello"
657 }
658 });
659
660 let set_msg: ClientMessage = serde_json::from_value(set_json).expect("deserialize set");
661 let keep_msg: ClientMessage = serde_json::from_value(keep_json).expect("deserialize keep");
662
663 match set_msg {
664 ClientMessage::SendMessage {
665 runtime_mode,
666 model_override,
667 ..
668 } => {
669 assert_eq!(runtime_mode, Some(RuntimeMode::Agent));
670 assert!(model_override.is_some());
671 }
672 _ => panic!("expected SendMessage"),
673 }
674
675 match keep_msg {
676 ClientMessage::SendMessage { model_override, .. } => {
677 assert_eq!(model_override, None);
678 }
679 _ => panic!("expected SendMessage"),
680 }
681 }
682
683 #[test]
684 fn update_working_directories_round_trip_full_and_empty() {
685 let demo_agents_md = r#"# Demo project
686
687- Keep changes small.
688- Run `cargo test`.
689"#
690 .trim()
691 .to_string();
692
693 let full = ClientMessage::UpdateWorkingDirectories {
694 working_directories: vec![WorkingDirectory {
695 path: "/Users/dev/project".to_string(),
696 git_branch: None,
697 agents_md: demo_agents_md.clone(),
698 rules: vec![Rule {
699 name: "Test after changes".to_string(),
700 description: "Run the relevant tests before finishing.".to_string(),
701 text: None,
702 always_apply: true,
703 }],
704 skills: vec![Skill {
705 name: "Build skill".to_string(),
706 description: "Run and fix build failures".to_string(),
707 }],
708 }],
709 };
710 let empty = ClientMessage::UpdateWorkingDirectories {
711 working_directories: vec![],
712 };
713
714 let full_json = serde_json::to_value(&full).expect("serialize full");
715 let empty_json = serde_json::to_value(&empty).expect("serialize empty");
716
717 let full_back: ClientMessage = serde_json::from_value(full_json).expect("deserialize full");
718 let empty_back: ClientMessage =
719 serde_json::from_value(empty_json).expect("deserialize empty");
720
721 match full_back {
722 ClientMessage::UpdateWorkingDirectories {
723 working_directories,
724 } => {
725 assert_eq!(working_directories.len(), 1);
726 assert_eq!(working_directories[0].path, "/Users/dev/project");
727 assert_eq!(working_directories[0].agents_md, demo_agents_md);
728 assert_eq!(working_directories[0].rules.len(), 1);
729 assert_eq!(working_directories[0].rules[0].name, "Test after changes");
730 assert_eq!(
731 working_directories[0].rules[0].description,
732 "Run the relevant tests before finishing."
733 );
734 assert!(working_directories[0].rules[0].always_apply);
735 assert_eq!(working_directories[0].skills.len(), 1);
736 assert_eq!(working_directories[0].skills[0].name, "Build skill");
737 assert_eq!(
738 working_directories[0].skills[0].description,
739 "Run and fix build failures"
740 );
741 }
742 _ => panic!("expected UpdateWorkingDirectories"),
743 }
744
745 match empty_back {
746 ClientMessage::UpdateWorkingDirectories {
747 working_directories,
748 } => {
749 assert!(working_directories.is_empty());
750 }
751 _ => panic!("expected UpdateWorkingDirectories"),
752 }
753 }
754
755 #[test]
756 fn update_working_directories_defaults_missing_nested_fields() {
757 let json = json!({
758 "UpdateWorkingDirectories": {
759 "working_directories": [{
760 "path": "/Users/dev/project"
761 }]
762 }
763 });
764
765 let back: ClientMessage = serde_json::from_value(json).expect("deserialize");
766
767 match back {
768 ClientMessage::UpdateWorkingDirectories {
769 working_directories,
770 } => {
771 assert_eq!(working_directories.len(), 1);
772 assert_eq!(working_directories[0].path, "/Users/dev/project");
773 assert!(working_directories[0].agents_md.is_empty());
774 assert!(working_directories[0].rules.is_empty());
775 assert!(working_directories[0].skills.is_empty());
776 }
777 _ => panic!("expected UpdateWorkingDirectories"),
778 }
779 }
780
781 #[test]
782 fn update_background_shells_round_trip_full_and_empty() {
783 let full = ClientMessage::UpdateBackgroundShells {
784 shells: vec![
785 background_shell_snapshot("bg_1", BackgroundShellStatus::Running),
786 background_shell_snapshot("bg_2", BackgroundShellStatus::Exited),
787 ],
788 };
789 let empty = ClientMessage::UpdateBackgroundShells { shells: vec![] };
790
791 let full_json = serde_json::to_value(&full).expect("serialize full");
792 let empty_json = serde_json::to_value(&empty).expect("serialize empty");
793
794 let full_back: ClientMessage = serde_json::from_value(full_json).expect("deserialize full");
795 let empty_back: ClientMessage =
796 serde_json::from_value(empty_json).expect("deserialize empty");
797
798 match full_back {
799 ClientMessage::UpdateBackgroundShells { shells } => {
800 assert_eq!(shells.len(), 2);
801 assert_eq!(shells[0].shell_id, "bg_1");
802 assert_eq!(shells[0].status, BackgroundShellStatus::Running);
803 assert_eq!(shells[0].exit_code, None);
804 assert_eq!(shells[1].status, BackgroundShellStatus::Exited);
805 assert_eq!(shells[1].exit_code, Some(0));
806 }
807 _ => panic!("expected UpdateBackgroundShells"),
808 }
809
810 match empty_back {
811 ClientMessage::UpdateBackgroundShells { shells } => {
812 assert!(shells.is_empty());
813 }
814 _ => panic!("expected UpdateBackgroundShells"),
815 }
816 }
817
818 #[test]
819 fn resolve_subagent_escalation_approved_round_trip() {
820 let msg = ClientMessage::ResolveSubagentEscalation {
821 parent_message_id: Uuid::nil(),
822 subagent_run_id: Uuid::nil(),
823 escalation_id: "esc-0".to_string(),
824 resolution: SubagentEscalationResolution::Approved,
825 };
826
827 let value = serde_json::to_value(&msg).expect("serialize");
828 let back: ClientMessage = serde_json::from_value(value).expect("deserialize");
829 match back {
830 ClientMessage::ResolveSubagentEscalation {
831 escalation_id,
832 resolution: SubagentEscalationResolution::Approved,
833 ..
834 } => assert_eq!(escalation_id, "esc-0"),
835 _ => panic!("expected ResolveSubagentEscalation::Approved"),
836 }
837 }
838
839 #[test]
840 fn resolve_subagent_escalation_rejected_round_trip() {
841 let msg = ClientMessage::ResolveSubagentEscalation {
842 parent_message_id: Uuid::nil(),
843 subagent_run_id: Uuid::nil(),
844 escalation_id: "esc-1".to_string(),
845 resolution: SubagentEscalationResolution::Rejected {
846 reason: Some("not now".to_string()),
847 },
848 };
849
850 let value = serde_json::to_value(&msg).expect("serialize");
851 let body = value
852 .get("ResolveSubagentEscalation")
853 .and_then(|v| v.as_object())
854 .expect("ResolveSubagentEscalation body");
855 assert_eq!(body.get("escalation_id"), Some(&json!("esc-1")));
856
857 let back: ClientMessage = serde_json::from_value(value).expect("deserialize");
858 match back {
859 ClientMessage::ResolveSubagentEscalation {
860 resolution:
861 SubagentEscalationResolution::Rejected {
862 reason: Some(reason),
863 },
864 ..
865 } => assert_eq!(reason, "not now"),
866 _ => panic!("expected ResolveSubagentEscalation::Rejected"),
867 }
868 }
869
870 #[test]
871 fn resolve_subagent_escalation_resolved_with_output_round_trip() {
872 let msg = ClientMessage::ResolveSubagentEscalation {
873 parent_message_id: Uuid::nil(),
874 subagent_run_id: Uuid::nil(),
875 escalation_id: "esc-2".to_string(),
876 resolution: SubagentEscalationResolution::ResolvedWithOutput {
877 is_error: false,
878 output: "ok".to_string(),
879 duration_seconds: Some(3),
880 },
881 };
882
883 let value = serde_json::to_value(&msg).expect("serialize");
884 let back: ClientMessage = serde_json::from_value(value).expect("deserialize");
885 match back {
886 ClientMessage::ResolveSubagentEscalation {
887 escalation_id,
888 resolution:
889 SubagentEscalationResolution::ResolvedWithOutput {
890 is_error,
891 output,
892 duration_seconds,
893 },
894 ..
895 } => {
896 assert_eq!(escalation_id, "esc-2");
897 assert!(!is_error);
898 assert_eq!(output, "ok");
899 assert_eq!(duration_seconds, Some(3));
900 }
901 _ => panic!("expected ResolveSubagentEscalation::ResolvedWithOutput"),
902 }
903 }
904}