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