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, 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        /// Random per-client-instance id (generated on client startup).
95        ///
96        /// Used for multi-client tool-call claiming/coordination. Not derived from auth token.
97        client_instance_id: String,
98        /// Client version.
99        version: String,
100        /// Minimum supported server version (the client can work with any server >= this).
101        min_supported_version: String,
102    },
103    /// Send a user message.
104    ///
105    /// If `thread_id` is `None`, the server creates a new thread and returns it in `SendMessageAck`.
106    /// `request_id` is a client-generated correlation id for 1:1 request↔response mapping.
107    SendMessage {
108        request_id: Uuid,
109        thread_id: Option<Uuid>,
110        text: String,
111        /// Optional runtime mode update to apply to the thread before generation.
112        #[serde(default, skip_serializing_if = "Option::is_none")]
113        runtime_mode: Option<RuntimeMode>,
114        /// Optional model override update to apply to the thread before generation.
115        ///
116        /// - `None`: do not change current override.
117        /// - `Some(value)`: set override to `value`.
118        #[serde(default, skip_serializing_if = "Option::is_none")]
119        model_override: Option<ThreadModelOverride>,
120    },
121    /// Update/refresh auth token without reconnecting the WebSocket.
122    UpdateAuthToken {
123        token: String,
124    },
125    /// Update the client's workspace roots / default root (used for prompt context + validations).
126    ///
127    /// Sent separately from `HelloMath` so roots can be updated without reconnecting.
128    UpdateWorkspaceRoots {
129        default_root: String,
130        workspace_roots: Vec<String>,
131    },
132    /// Replace the client-local skill snapshot exposed to the server for this connection.
133    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    /// Cancel the current in-progress generation for a specific assistant message.
154    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    /// The agent is waiting for the client/user to provide more input (e.g. tool outputs / approvals)
177    /// before it can continue generation.
178    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}