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)]
69pub enum ClientMessage {
70 HelloMath {
71 client_instance_id: String,
75 version: String,
77 min_supported_version: String,
79 },
80 SendMessage {
85 request_id: Uuid,
86 thread_id: Option<Uuid>,
87 text: String,
88 #[serde(default, skip_serializing_if = "Option::is_none")]
90 runtime_mode: Option<RuntimeMode>,
91 #[serde(default, skip_serializing_if = "Option::is_none")]
96 model_override: Option<ThreadModelOverride>,
97 },
98 UpdateAuthToken {
100 token: String,
101 },
102 UpdateWorkspaceRoots {
106 default_root: String,
107 workspace_roots: Vec<String>,
108 },
109 RejectToolCall {
110 id: String,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
112 reason: Option<String>,
113 },
114 AcceptToolCall {
115 id: String,
116 },
117 ToolCallOutputs {
118 outputs: Vec<ToolCallOutput>,
119 },
120 CancelGeneration {
122 message_id: Uuid,
123 },
124}
125
126#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
127pub struct Usage {
128 pub input_tokens: i32,
129 pub output_tokens: i32,
130 #[serde(default)]
131 pub cache_read_input_tokens: i32,
132 #[serde(default)]
133 pub cache_creation_input_tokens: i32,
134 #[serde(default)]
135 pub cache_creation_input_tokens_5m: i32,
136 #[serde(default)]
137 pub cache_creation_input_tokens_1h: i32,
138}
139
140#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
141pub enum MessageStatus {
142 Completed,
143 WaitingForUser,
146 Failed,
147 Cancelled,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub enum ServerMessage {
152 HelloMagic {
153 version: String,
154 min_supported_version: String,
155 },
156 VersionMismatch {
157 server_version: String,
158 server_min_supported_version: String,
159 },
160 Goodbye {
161 reconnect: bool,
162 },
163 SendMessageAck {
164 request_id: Uuid,
165 thread_id: Uuid,
166 user_message_id: Uuid,
167 },
168 AuthUpdated,
169 RuntimeModeUpdated {
170 thread_id: Uuid,
171 mode: RuntimeMode,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
173 changed_by_client_instance_id: Option<String>,
174 },
175 ThreadModelUpdated {
176 thread_id: Uuid,
177 #[serde(default, skip_serializing_if = "Option::is_none")]
178 model_override: Option<ThreadModelOverride>,
179 #[serde(default, skip_serializing_if = "Option::is_none")]
180 changed_by_client_instance_id: Option<String>,
181 },
182 MessageHeader {
183 message_id: Uuid,
184 thread_id: Uuid,
185 role: Role,
186 #[serde(default, skip_serializing_if = "Option::is_none")]
187 request_id: Option<Uuid>,
188 },
189 ReasoningDelta {
190 message_id: Uuid,
191 content: String,
192 },
193 TextDelta {
194 message_id: Uuid,
195 content: String,
196 },
197 ToolCallHeader {
198 message_id: Uuid,
199 tool_call_id: String,
200 name: String,
201 execution_target: ToolExecutionTarget,
202 approval: ToolCallApproval,
203 },
204 ToolCallArgumentsDelta {
205 message_id: Uuid,
206 tool_call_id: String,
207 delta: String,
208 },
209 ToolCall {
210 message_id: Uuid,
211 tool_call_id: String,
212 args: Value,
213 },
214 ToolCallResult {
215 message_id: Uuid,
216 tool_call_id: String,
217 is_error: bool,
218 output: String,
219 #[serde(default, skip_serializing_if = "Option::is_none")]
220 duration_seconds: Option<i32>,
221 },
222 ToolCallClaimed {
223 message_id: Uuid,
224 tool_call_id: String,
225 claimed_by_client_instance_id: String,
226 },
227 ToolCallApprovalUpdated {
228 message_id: Uuid,
229 tool_call_id: String,
230 approval: ToolCallApproval,
231 },
232 MessageDone {
233 message_id: Uuid,
234 #[serde(default, skip_serializing_if = "Option::is_none")]
235 usage: Option<Usage>,
236 status: MessageStatus,
237 },
238 Error {
239 #[serde(default, skip_serializing_if = "Option::is_none")]
240 request_id: Option<Uuid>,
241 #[serde(default, skip_serializing_if = "Option::is_none")]
242 message_id: Option<Uuid>,
243 code: String,
244 message: String,
245 },
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use serde_json::json;
252
253 #[test]
254 fn send_message_omits_optional_updates_when_not_set() {
255 let msg = ClientMessage::SendMessage {
256 request_id: Uuid::nil(),
257 thread_id: None,
258 text: "hello".to_string(),
259 runtime_mode: None,
260 model_override: None,
261 };
262
263 let value = serde_json::to_value(msg).expect("serialize");
264 let body = value
265 .get("SendMessage")
266 .and_then(|v| v.as_object())
267 .expect("SendMessage body");
268
269 assert!(body.get("runtime_mode").is_none());
270 assert!(body.get("model_override").is_none());
271 }
272
273 #[test]
274 fn send_message_serializes_model_override_when_set() {
275 let msg = ClientMessage::SendMessage {
276 request_id: Uuid::nil(),
277 thread_id: Some(Uuid::nil()),
278 text: "hello".to_string(),
279 runtime_mode: Some(RuntimeMode::Plan),
280 model_override: Some(ThreadModelOverride {
281 model_id: Uuid::nil(),
282 model_config: ModelConfig::default(),
283 }),
284 };
285
286 let value = serde_json::to_value(msg).expect("serialize");
287 let body = value
288 .get("SendMessage")
289 .and_then(|v| v.as_object())
290 .expect("SendMessage body");
291
292 assert_eq!(body.get("runtime_mode"), Some(&json!("Plan")));
293 assert!(body.get("model_override").is_some());
294 }
295
296 #[test]
297 fn send_message_deserializes_model_override_states() {
298 let set_json = json!({
299 "SendMessage": {
300 "request_id": Uuid::nil(),
301 "thread_id": Uuid::nil(),
302 "text": "hello",
303 "runtime_mode": "Agent",
304 "model_override": {
305 "model_id": Uuid::nil(),
306 "model_config": {}
307 }
308 }
309 });
310 let keep_json = json!({
311 "SendMessage": {
312 "request_id": Uuid::nil(),
313 "thread_id": Uuid::nil(),
314 "text": "hello"
315 }
316 });
317
318 let set_msg: ClientMessage = serde_json::from_value(set_json).expect("deserialize set");
319 let keep_msg: ClientMessage = serde_json::from_value(keep_json).expect("deserialize keep");
320
321 match set_msg {
322 ClientMessage::SendMessage {
323 runtime_mode,
324 model_override,
325 ..
326 } => {
327 assert_eq!(runtime_mode, Some(RuntimeMode::Agent));
328 assert!(model_override.is_some());
329 }
330 _ => panic!("expected SendMessage"),
331 }
332
333 match keep_msg {
334 ClientMessage::SendMessage { model_override, .. } => {
335 assert_eq!(model_override, None);
336 }
337 _ => panic!("expected SendMessage"),
338 }
339 }
340}