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