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 WorkspaceRoot {
104 pub cwd: 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 UpdateWorkspaceRoots {
251 workspace_roots: Vec<WorkspaceRoot>,
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_workspace_roots_round_trip_full_and_empty() {
685 let demo_agents_md = r#"# Demo workspace
686
687- Keep changes small.
688- Run `cargo test`.
689"#
690 .trim()
691 .to_string();
692
693 let full = ClientMessage::UpdateWorkspaceRoots {
694 workspace_roots: vec![WorkspaceRoot {
695 cwd: "/Users/dev/project".to_string(),
696 agents_md: demo_agents_md.clone(),
697 rules: vec![Rule {
698 name: "Test after changes".to_string(),
699 description: "Run the relevant tests before finishing.".to_string(),
700 text: None,
701 always_apply: true,
702 }],
703 skills: vec![Skill {
704 name: "Build skill".to_string(),
705 description: "Run and fix build failures".to_string(),
706 }],
707 }],
708 };
709 let empty = ClientMessage::UpdateWorkspaceRoots {
710 workspace_roots: vec![],
711 };
712
713 let full_json = serde_json::to_value(&full).expect("serialize full");
714 let empty_json = serde_json::to_value(&empty).expect("serialize empty");
715
716 let full_back: ClientMessage = serde_json::from_value(full_json).expect("deserialize full");
717 let empty_back: ClientMessage =
718 serde_json::from_value(empty_json).expect("deserialize empty");
719
720 match full_back {
721 ClientMessage::UpdateWorkspaceRoots { workspace_roots } => {
722 assert_eq!(workspace_roots.len(), 1);
723 assert_eq!(workspace_roots[0].cwd, "/Users/dev/project");
724 assert_eq!(workspace_roots[0].agents_md, demo_agents_md);
725 assert_eq!(workspace_roots[0].rules.len(), 1);
726 assert_eq!(workspace_roots[0].rules[0].name, "Test after changes");
727 assert_eq!(
728 workspace_roots[0].rules[0].description,
729 "Run the relevant tests before finishing."
730 );
731 assert!(workspace_roots[0].rules[0].always_apply);
732 assert_eq!(workspace_roots[0].skills.len(), 1);
733 assert_eq!(workspace_roots[0].skills[0].name, "Build skill");
734 assert_eq!(
735 workspace_roots[0].skills[0].description,
736 "Run and fix build failures"
737 );
738 }
739 _ => panic!("expected UpdateWorkspaceRoots"),
740 }
741
742 match empty_back {
743 ClientMessage::UpdateWorkspaceRoots { workspace_roots } => {
744 assert!(workspace_roots.is_empty());
745 }
746 _ => panic!("expected UpdateWorkspaceRoots"),
747 }
748 }
749
750 #[test]
751 fn update_workspace_roots_defaults_missing_nested_fields() {
752 let json = json!({
753 "UpdateWorkspaceRoots": {
754 "workspace_roots": [{
755 "cwd": "/Users/dev/project"
756 }]
757 }
758 });
759
760 let back: ClientMessage = serde_json::from_value(json).expect("deserialize");
761
762 match back {
763 ClientMessage::UpdateWorkspaceRoots { workspace_roots } => {
764 assert_eq!(workspace_roots.len(), 1);
765 assert_eq!(workspace_roots[0].cwd, "/Users/dev/project");
766 assert!(workspace_roots[0].agents_md.is_empty());
767 assert!(workspace_roots[0].rules.is_empty());
768 assert!(workspace_roots[0].skills.is_empty());
769 }
770 _ => panic!("expected UpdateWorkspaceRoots"),
771 }
772 }
773
774 #[test]
775 fn update_background_shells_round_trip_full_and_empty() {
776 let full = ClientMessage::UpdateBackgroundShells {
777 shells: vec![
778 background_shell_snapshot("bg_1", BackgroundShellStatus::Running),
779 background_shell_snapshot("bg_2", BackgroundShellStatus::Exited),
780 ],
781 };
782 let empty = ClientMessage::UpdateBackgroundShells { shells: vec![] };
783
784 let full_json = serde_json::to_value(&full).expect("serialize full");
785 let empty_json = serde_json::to_value(&empty).expect("serialize empty");
786
787 let full_back: ClientMessage = serde_json::from_value(full_json).expect("deserialize full");
788 let empty_back: ClientMessage =
789 serde_json::from_value(empty_json).expect("deserialize empty");
790
791 match full_back {
792 ClientMessage::UpdateBackgroundShells { shells } => {
793 assert_eq!(shells.len(), 2);
794 assert_eq!(shells[0].shell_id, "bg_1");
795 assert_eq!(shells[0].status, BackgroundShellStatus::Running);
796 assert_eq!(shells[0].exit_code, None);
797 assert_eq!(shells[1].status, BackgroundShellStatus::Exited);
798 assert_eq!(shells[1].exit_code, Some(0));
799 }
800 _ => panic!("expected UpdateBackgroundShells"),
801 }
802
803 match empty_back {
804 ClientMessage::UpdateBackgroundShells { shells } => {
805 assert!(shells.is_empty());
806 }
807 _ => panic!("expected UpdateBackgroundShells"),
808 }
809 }
810
811 #[test]
812 fn resolve_subagent_escalation_approved_round_trip() {
813 let msg = ClientMessage::ResolveSubagentEscalation {
814 parent_message_id: Uuid::nil(),
815 subagent_run_id: Uuid::nil(),
816 escalation_id: "esc-0".to_string(),
817 resolution: SubagentEscalationResolution::Approved,
818 };
819
820 let value = serde_json::to_value(&msg).expect("serialize");
821 let back: ClientMessage = serde_json::from_value(value).expect("deserialize");
822 match back {
823 ClientMessage::ResolveSubagentEscalation {
824 escalation_id,
825 resolution: SubagentEscalationResolution::Approved,
826 ..
827 } => assert_eq!(escalation_id, "esc-0"),
828 _ => panic!("expected ResolveSubagentEscalation::Approved"),
829 }
830 }
831
832 #[test]
833 fn resolve_subagent_escalation_rejected_round_trip() {
834 let msg = ClientMessage::ResolveSubagentEscalation {
835 parent_message_id: Uuid::nil(),
836 subagent_run_id: Uuid::nil(),
837 escalation_id: "esc-1".to_string(),
838 resolution: SubagentEscalationResolution::Rejected {
839 reason: Some("not now".to_string()),
840 },
841 };
842
843 let value = serde_json::to_value(&msg).expect("serialize");
844 let body = value
845 .get("ResolveSubagentEscalation")
846 .and_then(|v| v.as_object())
847 .expect("ResolveSubagentEscalation body");
848 assert_eq!(body.get("escalation_id"), Some(&json!("esc-1")));
849
850 let back: ClientMessage = serde_json::from_value(value).expect("deserialize");
851 match back {
852 ClientMessage::ResolveSubagentEscalation {
853 resolution:
854 SubagentEscalationResolution::Rejected {
855 reason: Some(reason),
856 },
857 ..
858 } => assert_eq!(reason, "not now"),
859 _ => panic!("expected ResolveSubagentEscalation::Rejected"),
860 }
861 }
862
863 #[test]
864 fn resolve_subagent_escalation_resolved_with_output_round_trip() {
865 let msg = ClientMessage::ResolveSubagentEscalation {
866 parent_message_id: Uuid::nil(),
867 subagent_run_id: Uuid::nil(),
868 escalation_id: "esc-2".to_string(),
869 resolution: SubagentEscalationResolution::ResolvedWithOutput {
870 is_error: false,
871 output: "ok".to_string(),
872 duration_seconds: Some(3),
873 },
874 };
875
876 let value = serde_json::to_value(&msg).expect("serialize");
877 let back: ClientMessage = serde_json::from_value(value).expect("deserialize");
878 match back {
879 ClientMessage::ResolveSubagentEscalation {
880 escalation_id,
881 resolution:
882 SubagentEscalationResolution::ResolvedWithOutput {
883 is_error,
884 output,
885 duration_seconds,
886 },
887 ..
888 } => {
889 assert_eq!(escalation_id, "esc-2");
890 assert!(!is_error);
891 assert_eq!(output, "ok");
892 assert_eq!(duration_seconds, Some(3));
893 }
894 _ => panic!("expected ResolveSubagentEscalation::ResolvedWithOutput"),
895 }
896 }
897}