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