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 struct LocalSkillMetadata {
70 pub skill_id: String,
71 pub name: String,
72 pub description: String,
73 pub cwd: String,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub enum ClientMessage {
78 HelloMath {
79 client_instance_id: String,
83 version: String,
85 min_supported_version: String,
87 },
88 SendMessage {
93 request_id: Uuid,
94 thread_id: Option<Uuid>,
95 text: String,
96 #[serde(default, skip_serializing_if = "Option::is_none")]
98 runtime_mode: Option<RuntimeMode>,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
104 model_override: Option<ThreadModelOverride>,
105 },
106 UpdateAuthToken {
108 token: String,
109 },
110 UpdateWorkspaceRoots {
114 default_root: String,
115 workspace_roots: Vec<String>,
116 },
117 UpdateLocalSkills {
119 skills: Vec<LocalSkillMetadata>,
120 },
121 RejectToolCall {
122 id: String,
123 #[serde(default, skip_serializing_if = "Option::is_none")]
124 reason: Option<String>,
125 },
126 AcceptToolCall {
127 id: String,
128 },
129 ToolCallOutputs {
130 outputs: Vec<ToolCallOutput>,
131 },
132 CancelGeneration {
134 message_id: Uuid,
135 },
136}
137
138#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
139pub struct Usage {
140 pub input_tokens: i32,
141 pub output_tokens: i32,
142 #[serde(default)]
143 pub cache_read_input_tokens: i32,
144 #[serde(default)]
145 pub cache_creation_input_tokens: i32,
146 #[serde(default)]
147 pub cache_creation_input_tokens_5m: i32,
148 #[serde(default)]
149 pub cache_creation_input_tokens_1h: i32,
150}
151
152#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
153pub enum MessageStatus {
154 Completed,
155 WaitingForUser,
158 Failed,
159 Cancelled,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub enum ServerMessage {
164 HelloMagic {
165 version: String,
166 min_supported_version: String,
167 },
168 VersionMismatch {
169 server_version: String,
170 server_min_supported_version: String,
171 },
172 Goodbye {
173 reconnect: bool,
174 },
175 SendMessageAck {
176 request_id: Uuid,
177 thread_id: Uuid,
178 user_message_id: Uuid,
179 },
180 AuthUpdated,
181 RuntimeModeUpdated {
182 thread_id: Uuid,
183 mode: RuntimeMode,
184 #[serde(default, skip_serializing_if = "Option::is_none")]
185 changed_by_client_instance_id: Option<String>,
186 },
187 ThreadModelUpdated {
188 thread_id: Uuid,
189 #[serde(default, skip_serializing_if = "Option::is_none")]
190 model_override: Option<ThreadModelOverride>,
191 #[serde(default, skip_serializing_if = "Option::is_none")]
192 changed_by_client_instance_id: Option<String>,
193 },
194 MessageHeader {
195 message_id: Uuid,
196 thread_id: Uuid,
197 role: Role,
198 #[serde(default, skip_serializing_if = "Option::is_none")]
199 request_id: Option<Uuid>,
200 },
201 ReasoningDelta {
202 message_id: Uuid,
203 content: String,
204 },
205 TextDelta {
206 message_id: Uuid,
207 content: String,
208 },
209 ToolCallHeader {
210 message_id: Uuid,
211 tool_call_id: String,
212 name: String,
213 execution_target: ToolExecutionTarget,
214 approval: ToolCallApproval,
215 },
216 ToolCallArgumentsDelta {
217 message_id: Uuid,
218 tool_call_id: String,
219 delta: String,
220 },
221 ToolCall {
222 message_id: Uuid,
223 tool_call_id: String,
224 args: Value,
225 },
226 ToolCallResult {
227 message_id: Uuid,
228 tool_call_id: String,
229 is_error: bool,
230 output: String,
231 #[serde(default, skip_serializing_if = "Option::is_none")]
232 duration_seconds: Option<i32>,
233 },
234 ToolCallClaimed {
235 message_id: Uuid,
236 tool_call_id: String,
237 claimed_by_client_instance_id: String,
238 },
239 ToolCallApprovalUpdated {
240 message_id: Uuid,
241 tool_call_id: String,
242 approval: ToolCallApproval,
243 },
244 MessageDone {
245 message_id: Uuid,
246 #[serde(default, skip_serializing_if = "Option::is_none")]
247 usage: Option<Usage>,
248 status: MessageStatus,
249 },
250 Error {
251 #[serde(default, skip_serializing_if = "Option::is_none")]
252 request_id: Option<Uuid>,
253 #[serde(default, skip_serializing_if = "Option::is_none")]
254 message_id: Option<Uuid>,
255 code: String,
256 message: String,
257 },
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263 use serde_json::json;
264
265 #[test]
266 fn send_message_omits_optional_updates_when_not_set() {
267 let msg = ClientMessage::SendMessage {
268 request_id: Uuid::nil(),
269 thread_id: None,
270 text: "hello".to_string(),
271 runtime_mode: None,
272 model_override: None,
273 };
274
275 let value = serde_json::to_value(msg).expect("serialize");
276 let body = value
277 .get("SendMessage")
278 .and_then(|v| v.as_object())
279 .expect("SendMessage body");
280
281 assert!(body.get("runtime_mode").is_none());
282 assert!(body.get("model_override").is_none());
283 }
284
285 #[test]
286 fn send_message_serializes_model_override_when_set() {
287 let msg = ClientMessage::SendMessage {
288 request_id: Uuid::nil(),
289 thread_id: Some(Uuid::nil()),
290 text: "hello".to_string(),
291 runtime_mode: Some(RuntimeMode::Plan),
292 model_override: Some(ThreadModelOverride {
293 model_id: Uuid::nil(),
294 model_config: ModelConfig::default(),
295 }),
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_eq!(body.get("runtime_mode"), Some(&json!("Plan")));
305 assert!(body.get("model_override").is_some());
306 }
307
308 #[test]
309 fn send_message_deserializes_model_override_states() {
310 let set_json = json!({
311 "SendMessage": {
312 "request_id": Uuid::nil(),
313 "thread_id": Uuid::nil(),
314 "text": "hello",
315 "runtime_mode": "Agent",
316 "model_override": {
317 "model_id": Uuid::nil(),
318 "model_config": {}
319 }
320 }
321 });
322 let keep_json = json!({
323 "SendMessage": {
324 "request_id": Uuid::nil(),
325 "thread_id": Uuid::nil(),
326 "text": "hello"
327 }
328 });
329
330 let set_msg: ClientMessage = serde_json::from_value(set_json).expect("deserialize set");
331 let keep_msg: ClientMessage = serde_json::from_value(keep_json).expect("deserialize keep");
332
333 match set_msg {
334 ClientMessage::SendMessage {
335 runtime_mode,
336 model_override,
337 ..
338 } => {
339 assert_eq!(runtime_mode, Some(RuntimeMode::Agent));
340 assert!(model_override.is_some());
341 }
342 _ => panic!("expected SendMessage"),
343 }
344
345 match keep_msg {
346 ClientMessage::SendMessage { model_override, .. } => {
347 assert_eq!(model_override, None);
348 }
349 _ => panic!("expected SendMessage"),
350 }
351 }
352
353 #[test]
354 fn update_local_skills_round_trip_full_and_empty() {
355 let full = ClientMessage::UpdateLocalSkills {
356 skills: vec![LocalSkillMetadata {
357 skill_id: "ls-abc123".to_string(),
358 name: "Build skill".to_string(),
359 description: "Run and fix build failures".to_string(),
360 cwd: "/Users/dev/project".to_string(),
361 }],
362 };
363 let empty = ClientMessage::UpdateLocalSkills { skills: vec![] };
364
365 let full_json = serde_json::to_value(&full).expect("serialize full");
366 let empty_json = serde_json::to_value(&empty).expect("serialize empty");
367
368 let full_back: ClientMessage = serde_json::from_value(full_json).expect("deserialize full");
369 let empty_back: ClientMessage =
370 serde_json::from_value(empty_json).expect("deserialize empty");
371
372 match full_back {
373 ClientMessage::UpdateLocalSkills { skills } => {
374 assert_eq!(skills.len(), 1);
375 assert_eq!(skills[0].skill_id, "ls-abc123");
376 assert_eq!(skills[0].name, "Build skill");
377 assert_eq!(skills[0].description, "Run and fix build failures");
378 assert_eq!(skills[0].cwd, "/Users/dev/project");
379 }
380 _ => panic!("expected UpdateLocalSkills"),
381 }
382
383 match empty_back {
384 ClientMessage::UpdateLocalSkills { skills } => {
385 assert!(skills.is_empty());
386 }
387 _ => panic!("expected UpdateLocalSkills"),
388 }
389 }
390}