Skip to main content

magic_coder_types/protocol/
types.rs

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    /// Tool must be executed by a connected client (TUI/VS Code).
21    ClientLocal,
22    /// Tool is executed server-side (agents/MCP/etc). Clients must not provide outputs.
23    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    /// Tool call id
60    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        /// Random per-client-instance id (generated on client startup).
72        ///
73        /// Used for multi-client tool-call claiming/coordination. Not derived from auth token.
74        client_instance_id: String,
75        /// Client version.
76        version: String,
77        /// Minimum supported server version (the client can work with any server >= this).
78        min_supported_version: String,
79    },
80    /// Send a user message.
81    ///
82    /// If `thread_id` is `None`, the server creates a new thread and returns it in `SendMessageAck`.
83    /// `request_id` is a client-generated correlation id for 1:1 request↔response mapping.
84    SendMessage {
85        request_id: Uuid,
86        thread_id: Option<Uuid>,
87        text: String,
88        /// Optional runtime mode update to apply to the thread before generation.
89        #[serde(default, skip_serializing_if = "Option::is_none")]
90        runtime_mode: Option<RuntimeMode>,
91        /// Optional model override update to apply to the thread before generation.
92        ///
93        /// - `None`: do not change current override.
94        /// - `Some(value)`: set override to `value`.
95        #[serde(default, skip_serializing_if = "Option::is_none")]
96        model_override: Option<ThreadModelOverride>,
97    },
98    /// Update/refresh auth token without reconnecting the WebSocket.
99    UpdateAuthToken {
100        token: String,
101    },
102    /// Update the client's workspace roots / default root (used for prompt context + validations).
103    ///
104    /// Sent separately from `HelloMath` so roots can be updated without reconnecting.
105    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    /// Cancel the current in-progress generation for a specific assistant message.
121    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    /// The agent is waiting for the client/user to provide more input (e.g. tool outputs / approvals)
144    /// before it can continue generation.
145    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}