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