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