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 skill_id: String,
86 pub name: String,
87 pub description: String,
88 pub cwd: String,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub enum ClientMessage {
93 HelloMath {
94 client_instance_id: String,
98 version: String,
100 min_supported_version: String,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
104 resume_thread_id: Option<Uuid>,
105 },
106 SendMessage {
111 request_id: Uuid,
112 thread_id: Option<Uuid>,
113 text: String,
114 #[serde(default, skip_serializing_if = "Option::is_none")]
116 runtime_mode: Option<RuntimeMode>,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
122 model_override: Option<ThreadModelOverride>,
123 },
124 UpdateAuthToken {
126 token: String,
127 },
128 UpdateWorkspaceRoots {
132 default_root: String,
133 workspace_roots: Vec<String>,
134 },
135 UpdateLocalSkills {
137 skills: Vec<LocalSkillMetadata>,
138 },
139 RejectToolCall {
140 id: String,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
142 reason: Option<String>,
143 },
144 AcceptToolCall {
145 id: String,
146 },
147 ResolveSubagentEscalation {
148 parent_message_id: Uuid,
149 subagent_run_id: Uuid,
150 escalation_id: String,
151 resolution: SubagentEscalationResolution,
152 },
153 ToolCallOutputs {
154 outputs: Vec<ToolCallOutput>,
155 },
156 CancelGeneration {
158 message_id: Uuid,
159 },
160}
161
162#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
163pub struct Usage {
164 pub input_tokens: i32,
165 pub output_tokens: i32,
166 #[serde(default)]
167 pub cache_read_input_tokens: i32,
168 #[serde(default)]
169 pub cache_creation_input_tokens: i32,
170 #[serde(default)]
171 pub cache_creation_input_tokens_5m: i32,
172 #[serde(default)]
173 pub cache_creation_input_tokens_1h: i32,
174}
175
176#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
177pub enum MessageStatus {
178 Completed,
179 WaitingForUser,
182 Failed,
183 Cancelled,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub enum ServerMessage {
188 HelloMagic {
189 version: String,
190 min_supported_version: String,
191 },
192 VersionMismatch {
193 server_version: String,
194 server_min_supported_version: String,
195 },
196 Goodbye {
197 reconnect: bool,
198 },
199 SendMessageAck {
200 request_id: Uuid,
201 thread_id: Uuid,
202 user_message_id: Uuid,
203 },
204 AuthUpdated,
205 RuntimeModeUpdated {
206 thread_id: Uuid,
207 mode: RuntimeMode,
208 #[serde(default, skip_serializing_if = "Option::is_none")]
209 changed_by_client_instance_id: Option<String>,
210 },
211 ThreadModelUpdated {
212 thread_id: Uuid,
213 #[serde(default, skip_serializing_if = "Option::is_none")]
214 model_override: Option<ThreadModelOverride>,
215 #[serde(default, skip_serializing_if = "Option::is_none")]
216 changed_by_client_instance_id: Option<String>,
217 },
218 MessageHeader {
219 message_id: Uuid,
220 thread_id: Uuid,
221 role: Role,
222 #[serde(default, skip_serializing_if = "Option::is_none")]
223 request_id: Option<Uuid>,
224 },
225 ReasoningDelta {
226 message_id: Uuid,
227 content: String,
228 },
229 TextDelta {
230 message_id: Uuid,
231 content: String,
232 },
233 ToolCallHeader {
234 message_id: Uuid,
235 tool_call_id: String,
236 name: String,
237 execution_target: ToolExecutionTarget,
238 approval: ToolCallApproval,
239 },
240 ToolCallArgumentsDelta {
241 message_id: Uuid,
242 tool_call_id: String,
243 delta: String,
244 },
245 ToolCall {
246 message_id: Uuid,
247 tool_call_id: String,
248 args: Value,
249 },
250 ToolCallResult {
251 message_id: Uuid,
252 tool_call_id: String,
253 is_error: bool,
254 output: String,
255 #[serde(default, skip_serializing_if = "Option::is_none")]
256 duration_seconds: Option<i32>,
257 },
258 ToolCallClaimed {
259 message_id: Uuid,
260 tool_call_id: String,
261 claimed_by_client_instance_id: String,
262 },
263 ToolCallApprovalUpdated {
264 message_id: Uuid,
265 tool_call_id: String,
266 approval: ToolCallApproval,
267 },
268 MessageDone {
269 message_id: Uuid,
270 #[serde(default, skip_serializing_if = "Option::is_none")]
271 usage: Option<Usage>,
272 status: MessageStatus,
273 },
274 Error {
275 #[serde(default, skip_serializing_if = "Option::is_none")]
276 request_id: Option<Uuid>,
277 #[serde(default, skip_serializing_if = "Option::is_none")]
278 message_id: Option<Uuid>,
279 code: String,
280 message: String,
281 },
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287 use serde_json::json;
288
289 #[test]
290 fn send_message_omits_optional_updates_when_not_set() {
291 let msg = ClientMessage::SendMessage {
292 request_id: Uuid::nil(),
293 thread_id: None,
294 text: "hello".to_string(),
295 runtime_mode: None,
296 model_override: None,
297 };
298
299 let value = serde_json::to_value(msg).expect("serialize");
300 let body = value
301 .get("SendMessage")
302 .and_then(|v| v.as_object())
303 .expect("SendMessage body");
304
305 assert!(body.get("runtime_mode").is_none());
306 assert!(body.get("model_override").is_none());
307 }
308
309 #[test]
310 fn hello_math_omits_resume_thread_id_when_not_set() {
311 let msg = ClientMessage::HelloMath {
312 client_instance_id: "client-a".to_string(),
313 version: "1.2.3".to_string(),
314 min_supported_version: "1.0.0".to_string(),
315 resume_thread_id: None,
316 };
317
318 let value = serde_json::to_value(msg).expect("serialize");
319 let body = value
320 .get("HelloMath")
321 .and_then(|v| v.as_object())
322 .expect("HelloMath body");
323
324 assert!(body.get("resume_thread_id").is_none());
325 }
326
327 #[test]
328 fn hello_math_round_trip_resume_thread_id() {
329 let thread_id = Uuid::new_v4();
330 let msg = ClientMessage::HelloMath {
331 client_instance_id: "client-a".to_string(),
332 version: "1.2.3".to_string(),
333 min_supported_version: "1.0.0".to_string(),
334 resume_thread_id: Some(thread_id),
335 };
336
337 let value = serde_json::to_value(&msg).expect("serialize");
338 let body = value
339 .get("HelloMath")
340 .and_then(|v| v.as_object())
341 .expect("HelloMath body");
342 assert_eq!(
343 body.get("resume_thread_id"),
344 Some(&serde_json::Value::String(thread_id.to_string()))
345 );
346
347 let back: ClientMessage = serde_json::from_value(value).expect("deserialize");
348 match back {
349 ClientMessage::HelloMath {
350 resume_thread_id, ..
351 } => assert_eq!(resume_thread_id, Some(thread_id)),
352 _ => panic!("expected HelloMath"),
353 }
354 }
355
356 #[test]
357 fn send_message_serializes_model_override_when_set() {
358 let msg = ClientMessage::SendMessage {
359 request_id: Uuid::nil(),
360 thread_id: Some(Uuid::nil()),
361 text: "hello".to_string(),
362 runtime_mode: Some(RuntimeMode::Plan),
363 model_override: Some(ThreadModelOverride {
364 model_id: Uuid::nil(),
365 model_config: ModelConfig::default(),
366 }),
367 };
368
369 let value = serde_json::to_value(msg).expect("serialize");
370 let body = value
371 .get("SendMessage")
372 .and_then(|v| v.as_object())
373 .expect("SendMessage body");
374
375 assert_eq!(body.get("runtime_mode"), Some(&json!("Plan")));
376 assert!(body.get("model_override").is_some());
377 }
378
379 #[test]
380 fn send_message_deserializes_model_override_states() {
381 let set_json = json!({
382 "SendMessage": {
383 "request_id": Uuid::nil(),
384 "thread_id": Uuid::nil(),
385 "text": "hello",
386 "runtime_mode": "Agent",
387 "model_override": {
388 "model_id": Uuid::nil(),
389 "model_config": {}
390 }
391 }
392 });
393 let keep_json = json!({
394 "SendMessage": {
395 "request_id": Uuid::nil(),
396 "thread_id": Uuid::nil(),
397 "text": "hello"
398 }
399 });
400
401 let set_msg: ClientMessage = serde_json::from_value(set_json).expect("deserialize set");
402 let keep_msg: ClientMessage = serde_json::from_value(keep_json).expect("deserialize keep");
403
404 match set_msg {
405 ClientMessage::SendMessage {
406 runtime_mode,
407 model_override,
408 ..
409 } => {
410 assert_eq!(runtime_mode, Some(RuntimeMode::Agent));
411 assert!(model_override.is_some());
412 }
413 _ => panic!("expected SendMessage"),
414 }
415
416 match keep_msg {
417 ClientMessage::SendMessage { model_override, .. } => {
418 assert_eq!(model_override, None);
419 }
420 _ => panic!("expected SendMessage"),
421 }
422 }
423
424 #[test]
425 fn update_local_skills_round_trip_full_and_empty() {
426 let full = ClientMessage::UpdateLocalSkills {
427 skills: vec![LocalSkillMetadata {
428 skill_id: "ls-abc123".to_string(),
429 name: "Build skill".to_string(),
430 description: "Run and fix build failures".to_string(),
431 cwd: "/Users/dev/project".to_string(),
432 }],
433 };
434 let empty = ClientMessage::UpdateLocalSkills { skills: vec![] };
435
436 let full_json = serde_json::to_value(&full).expect("serialize full");
437 let empty_json = serde_json::to_value(&empty).expect("serialize empty");
438
439 let full_back: ClientMessage = serde_json::from_value(full_json).expect("deserialize full");
440 let empty_back: ClientMessage =
441 serde_json::from_value(empty_json).expect("deserialize empty");
442
443 match full_back {
444 ClientMessage::UpdateLocalSkills { skills } => {
445 assert_eq!(skills.len(), 1);
446 assert_eq!(skills[0].skill_id, "ls-abc123");
447 assert_eq!(skills[0].name, "Build skill");
448 assert_eq!(skills[0].description, "Run and fix build failures");
449 assert_eq!(skills[0].cwd, "/Users/dev/project");
450 }
451 _ => panic!("expected UpdateLocalSkills"),
452 }
453
454 match empty_back {
455 ClientMessage::UpdateLocalSkills { skills } => {
456 assert!(skills.is_empty());
457 }
458 _ => panic!("expected UpdateLocalSkills"),
459 }
460 }
461
462 #[test]
463 fn resolve_subagent_escalation_rejected_round_trip() {
464 let msg = ClientMessage::ResolveSubagentEscalation {
465 parent_message_id: Uuid::nil(),
466 subagent_run_id: Uuid::nil(),
467 escalation_id: "esc-1".to_string(),
468 resolution: SubagentEscalationResolution::Rejected {
469 reason: Some("not now".to_string()),
470 },
471 };
472
473 let value = serde_json::to_value(&msg).expect("serialize");
474 let body = value
475 .get("ResolveSubagentEscalation")
476 .and_then(|v| v.as_object())
477 .expect("ResolveSubagentEscalation body");
478 assert_eq!(body.get("escalation_id"), Some(&json!("esc-1")));
479
480 let back: ClientMessage = serde_json::from_value(value).expect("deserialize");
481 match back {
482 ClientMessage::ResolveSubagentEscalation {
483 resolution:
484 SubagentEscalationResolution::Rejected {
485 reason: Some(reason),
486 },
487 ..
488 } => assert_eq!(reason, "not now"),
489 _ => panic!("expected ResolveSubagentEscalation::Rejected"),
490 }
491 }
492
493 #[test]
494 fn resolve_subagent_escalation_resolved_with_output_round_trip() {
495 let msg = ClientMessage::ResolveSubagentEscalation {
496 parent_message_id: Uuid::nil(),
497 subagent_run_id: Uuid::nil(),
498 escalation_id: "esc-2".to_string(),
499 resolution: SubagentEscalationResolution::ResolvedWithOutput {
500 is_error: false,
501 output: "ok".to_string(),
502 duration_seconds: Some(3),
503 },
504 };
505
506 let value = serde_json::to_value(&msg).expect("serialize");
507 let back: ClientMessage = serde_json::from_value(value).expect("deserialize");
508 match back {
509 ClientMessage::ResolveSubagentEscalation {
510 escalation_id,
511 resolution:
512 SubagentEscalationResolution::ResolvedWithOutput {
513 is_error,
514 output,
515 duration_seconds,
516 },
517 ..
518 } => {
519 assert_eq!(escalation_id, "esc-2");
520 assert!(!is_error);
521 assert_eq!(output, "ok");
522 assert_eq!(duration_seconds, Some(3));
523 }
524 _ => panic!("expected ResolveSubagentEscalation::ResolvedWithOutput"),
525 }
526 }
527}