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