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