Skip to main content

opendev_web/
protocol.rs

1//! WebSocket message type registry.
2//!
3//! Provides a canonical enum of all WebSocket message types used between the
4//! server and the React frontend. The `Display` / `Serialize` implementations
5//! produce the exact same JSON strings the frontend expects.
6
7use serde::{Deserialize, Serialize};
8use std::fmt;
9
10/// All known WebSocket message types, matching the Python `WSMessageType` enum.
11///
12/// Variants are grouped into server-to-client and client-to-server.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub enum WsMessageType {
15    // ── Server -> Client ────────────────────────────────────────────
16    /// A tool call is being made by the agent.
17    #[serde(rename = "tool_call")]
18    ToolCall,
19    /// Result of a tool call.
20    #[serde(rename = "tool_result")]
21    ToolResult,
22    /// An approval is required before executing a tool.
23    #[serde(rename = "approval_required")]
24    ApprovalRequired,
25    /// An approval request has been resolved.
26    #[serde(rename = "approval_resolved")]
27    ApprovalResolved,
28    /// The agent needs user input via ask-user.
29    #[serde(rename = "ask_user_required")]
30    AskUserRequired,
31    /// An ask-user request has been resolved.
32    #[serde(rename = "ask_user_resolved")]
33    AskUserResolved,
34    /// Plan content from the planning agent.
35    #[serde(rename = "plan_content")]
36    PlanContent,
37    /// A plan requires user approval.
38    #[serde(rename = "plan_approval_required")]
39    PlanApprovalRequired,
40    /// A plan approval request has been resolved.
41    #[serde(rename = "plan_approval_resolved")]
42    PlanApprovalResolved,
43    /// Status update (mode, autonomy, thinking, etc.).
44    #[serde(rename = "status_update")]
45    StatusUpdate,
46    /// The agent has completed its task.
47    #[serde(rename = "task_completed")]
48    TaskCompleted,
49    /// A subagent has started execution.
50    #[serde(rename = "subagent_start")]
51    SubagentStart,
52    /// A subagent has completed execution.
53    #[serde(rename = "subagent_complete")]
54    SubagentComplete,
55    /// Parallel agent execution started.
56    #[serde(rename = "parallel_agents_start")]
57    ParallelAgentsStart,
58    /// Parallel agent execution completed.
59    #[serde(rename = "parallel_agents_done")]
60    ParallelAgentsDone,
61    /// A thinking/reasoning block from the model.
62    #[serde(rename = "thinking_block")]
63    ThinkingBlock,
64    /// Progress update during a long-running operation.
65    #[serde(rename = "progress")]
66    Progress,
67    /// A tool call within a nested/subagent context.
68    #[serde(rename = "nested_tool_call")]
69    NestedToolCall,
70    /// Result of a nested tool call.
71    #[serde(rename = "nested_tool_result")]
72    NestedToolResult,
73    /// Streaming message chunk.
74    #[serde(rename = "message_chunk")]
75    MessageChunk,
76    /// Start of a new assistant message.
77    #[serde(rename = "message_start")]
78    MessageStart,
79    /// Completion of an assistant message.
80    #[serde(rename = "message_complete")]
81    MessageComplete,
82    /// Session activity event.
83    #[serde(rename = "session_activity")]
84    SessionActivity,
85    /// User message (echoed back or broadcast).
86    #[serde(rename = "user_message")]
87    UserMessage,
88    /// MCP server status changed.
89    #[serde(rename = "mcp:status_changed")]
90    McpStatusChanged,
91    /// MCP servers list updated.
92    #[serde(rename = "mcp:servers_updated")]
93    McpServersUpdated,
94    /// Error message.
95    #[serde(rename = "error")]
96    Error,
97    /// Pong response to a ping.
98    #[serde(rename = "pong")]
99    Pong,
100
101    // ── Client -> Server ────────────────────────────────────────────
102    /// Query / user message from the client.
103    #[serde(rename = "query")]
104    Query,
105    /// Approval response from the client.
106    #[serde(rename = "approve")]
107    Approve,
108    /// Ask-user response from the client.
109    #[serde(rename = "ask_user_response")]
110    AskUserResponse,
111    /// Plan approval response from the client.
112    #[serde(rename = "plan_approval_response")]
113    PlanApprovalResponse,
114    /// Ping keepalive.
115    #[serde(rename = "ping")]
116    Ping,
117    /// Interrupt request.
118    #[serde(rename = "interrupt")]
119    Interrupt,
120}
121
122impl WsMessageType {
123    /// Get the string representation matching the wire format.
124    pub fn as_str(&self) -> &'static str {
125        match self {
126            // Server -> Client
127            Self::ToolCall => "tool_call",
128            Self::ToolResult => "tool_result",
129            Self::ApprovalRequired => "approval_required",
130            Self::ApprovalResolved => "approval_resolved",
131            Self::AskUserRequired => "ask_user_required",
132            Self::AskUserResolved => "ask_user_resolved",
133            Self::PlanContent => "plan_content",
134            Self::PlanApprovalRequired => "plan_approval_required",
135            Self::PlanApprovalResolved => "plan_approval_resolved",
136            Self::StatusUpdate => "status_update",
137            Self::TaskCompleted => "task_completed",
138            Self::SubagentStart => "subagent_start",
139            Self::SubagentComplete => "subagent_complete",
140            Self::ParallelAgentsStart => "parallel_agents_start",
141            Self::ParallelAgentsDone => "parallel_agents_done",
142            Self::ThinkingBlock => "thinking_block",
143            Self::Progress => "progress",
144            Self::NestedToolCall => "nested_tool_call",
145            Self::NestedToolResult => "nested_tool_result",
146            Self::MessageChunk => "message_chunk",
147            Self::MessageStart => "message_start",
148            Self::MessageComplete => "message_complete",
149            Self::SessionActivity => "session_activity",
150            Self::UserMessage => "user_message",
151            Self::McpStatusChanged => "mcp:status_changed",
152            Self::McpServersUpdated => "mcp:servers_updated",
153            Self::Error => "error",
154            Self::Pong => "pong",
155            // Client -> Server
156            Self::Query => "query",
157            Self::Approve => "approve",
158            Self::AskUserResponse => "ask_user_response",
159            Self::PlanApprovalResponse => "plan_approval_response",
160            Self::Ping => "ping",
161            Self::Interrupt => "interrupt",
162        }
163    }
164
165    /// Parse a message type string into the enum.
166    pub fn from_str_opt(s: &str) -> Option<Self> {
167        match s {
168            "tool_call" => Some(Self::ToolCall),
169            "tool_result" => Some(Self::ToolResult),
170            "approval_required" => Some(Self::ApprovalRequired),
171            "approval_resolved" => Some(Self::ApprovalResolved),
172            "ask_user_required" => Some(Self::AskUserRequired),
173            "ask_user_resolved" => Some(Self::AskUserResolved),
174            "plan_content" => Some(Self::PlanContent),
175            "plan_approval_required" => Some(Self::PlanApprovalRequired),
176            "plan_approval_resolved" => Some(Self::PlanApprovalResolved),
177            "status_update" => Some(Self::StatusUpdate),
178            "task_completed" => Some(Self::TaskCompleted),
179            "subagent_start" => Some(Self::SubagentStart),
180            "subagent_complete" => Some(Self::SubagentComplete),
181            "parallel_agents_start" => Some(Self::ParallelAgentsStart),
182            "parallel_agents_done" => Some(Self::ParallelAgentsDone),
183            "thinking_block" => Some(Self::ThinkingBlock),
184            "progress" => Some(Self::Progress),
185            "nested_tool_call" => Some(Self::NestedToolCall),
186            "nested_tool_result" => Some(Self::NestedToolResult),
187            "message_chunk" => Some(Self::MessageChunk),
188            "message_start" => Some(Self::MessageStart),
189            "message_complete" => Some(Self::MessageComplete),
190            "session_activity" => Some(Self::SessionActivity),
191            "user_message" => Some(Self::UserMessage),
192            "mcp:status_changed" => Some(Self::McpStatusChanged),
193            "mcp:servers_updated" => Some(Self::McpServersUpdated),
194            "error" => Some(Self::Error),
195            "pong" => Some(Self::Pong),
196            "query" => Some(Self::Query),
197            "approve" => Some(Self::Approve),
198            "ask_user_response" => Some(Self::AskUserResponse),
199            "plan_approval_response" => Some(Self::PlanApprovalResponse),
200            "ping" => Some(Self::Ping),
201            "interrupt" => Some(Self::Interrupt),
202            _ => None,
203        }
204    }
205}
206
207impl fmt::Display for WsMessageType {
208    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209        f.write_str(self.as_str())
210    }
211}
212
213/// Construct a standard WebSocket message envelope.
214pub fn ws_message(msg_type: WsMessageType, data: serde_json::Value) -> serde_json::Value {
215    serde_json::json!({
216        "type": msg_type.as_str(),
217        "data": data,
218    })
219}
220
221#[cfg(test)]
222#[path = "protocol_tests.rs"]
223mod tests;