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