Skip to main content

codex_codes/
messages.rs

1//! Typed dispatch for app-server notifications and server-to-client requests.
2//!
3//! The Codex app-server speaks JSON-RPC where every message carries a
4//! `method` discriminant alongside a free-form `params` blob. This module
5//! lifts that loose envelope into closed enums — [`Notification`] for
6//! server-initiated notifications and [`ServerRequest`] for server-initiated
7//! requests (the approval flow). Each variant wraps a typed param struct
8//! from [`crate::protocol`].
9//!
10//! The pattern mirrors the [`ContentBlock`] dispatch in the sibling
11//! `claude-codes` crate: hand-written [`Serialize`]/[`Deserialize`] impls
12//! inspect the discriminant, route known cases through `serde_json::from_value`
13//! into the typed struct, and route unknown methods into an `Unknown`
14//! variant — preserving the raw payload for forward compatibility with
15//! future codex versions.
16//!
17//! ## Typing contract
18//!
19//! - Unknown methods route to [`Notification::Unknown`] / [`ServerRequest::Unknown`]
20//!   without error. Encountering one in production typically means the
21//!   installed Codex CLI is newer than the bindings.
22//! - Known methods whose payload fails to deserialize **do** cause an error.
23//!   If you see one, the typed binding in [`crate::protocol`] is out of
24//!   sync with the wire format and needs to be updated.
25
26use crate::jsonrpc::RequestId;
27use crate::protocol::{
28    methods, AccountLoginCompletedNotification, AccountRateLimitsUpdatedNotification,
29    AccountUpdatedNotification, AgentMessageDeltaNotification, AppListUpdatedNotification,
30    CommandExecOutputDeltaNotification, CommandExecutionOutputDeltaNotification,
31    CommandExecutionRequestApprovalParams, ConfigWarningNotification, ContextCompactedNotification,
32    DeprecationNoticeNotification, ErrorNotification,
33    ExternalAgentConfigImportCompletedNotification, FileChangeOutputDeltaNotification,
34    FileChangePatchUpdatedNotification, FileChangeRequestApprovalParams, FsChangedNotification,
35    FuzzyFileSearchSessionCompletedNotification, FuzzyFileSearchSessionUpdatedNotification,
36    GuardianWarningNotification, HookCompletedNotification, HookStartedNotification,
37    ItemCompletedNotification, ItemGuardianApprovalReviewCompletedNotification,
38    ItemGuardianApprovalReviewStartedNotification, ItemStartedNotification,
39    McpServerOauthLoginCompletedNotification, McpServerStatusUpdatedNotification,
40    McpToolCallProgressNotification, ModelReroutedNotification, ModelVerificationNotification,
41    PlanDeltaNotification, ProcessExitedNotification, ProcessOutputDeltaNotification,
42    ReasoningSummaryPartAddedNotification, ReasoningSummaryTextDeltaNotification,
43    ReasoningTextDeltaNotification, RemoteControlStatusChangedNotification,
44    ServerRequestResolvedNotification, SkillsChangedNotification, TerminalInteractionNotification,
45    ThreadArchivedNotification, ThreadClosedNotification, ThreadGoalClearedNotification,
46    ThreadGoalUpdatedNotification, ThreadNameUpdatedNotification, ThreadRealtimeClosedNotification,
47    ThreadRealtimeErrorNotification, ThreadRealtimeItemAddedNotification,
48    ThreadRealtimeOutputAudioDeltaNotification, ThreadRealtimeSdpNotification,
49    ThreadRealtimeStartedNotification, ThreadRealtimeTranscriptDeltaNotification,
50    ThreadRealtimeTranscriptDoneNotification, ThreadStartedNotification,
51    ThreadStatusChangedNotification, ThreadTokenUsageUpdatedNotification,
52    ThreadUnarchivedNotification, TurnCompletedNotification, TurnDiffUpdatedNotification,
53    TurnPlanUpdatedNotification, TurnStartedNotification, WarningNotification,
54    WindowsSandboxSetupCompletedNotification, WindowsWorldWritableWarningNotification,
55};
56use serde::{Deserialize, Deserializer, Serialize, Serializer};
57use serde_json::Value;
58
59/// A server-to-client notification.
60///
61/// Each variant maps to a single `method` string on the wire. The `Unknown`
62/// variant captures methods this crate version doesn't model yet, preserving
63/// the raw payload for inspection.
64#[derive(Debug, Clone)]
65pub enum Notification {
66    /// `thread/started`
67    ThreadStarted(ThreadStartedNotification),
68    /// `thread/status/changed`
69    ThreadStatusChanged(ThreadStatusChangedNotification),
70    /// `thread/tokenUsage/updated`
71    ThreadTokenUsageUpdated(ThreadTokenUsageUpdatedNotification),
72    /// `turn/started`
73    TurnStarted(TurnStartedNotification),
74    /// `turn/completed`
75    TurnCompleted(TurnCompletedNotification),
76    /// `item/started`
77    ItemStarted(ItemStartedNotification),
78    /// `item/completed`
79    ItemCompleted(ItemCompletedNotification),
80    /// `item/agentMessage/delta`
81    AgentMessageDelta(AgentMessageDeltaNotification),
82    /// `item/commandExecution/outputDelta`
83    CmdOutputDelta(CommandExecutionOutputDeltaNotification),
84    /// `item/fileChange/outputDelta`
85    FileChangeOutputDelta(FileChangeOutputDeltaNotification),
86    /// `item/reasoning/summaryTextDelta`
87    ReasoningDelta(ReasoningSummaryTextDeltaNotification),
88    /// `error`
89    Error(ErrorNotification),
90    /// `account/rateLimits/updated`
91    AccountRateLimitsUpdated(AccountRateLimitsUpdatedNotification),
92    /// `mcpServer/startupStatus/updated`
93    McpServerStartupStatusUpdated(McpServerStatusUpdatedNotification),
94    /// `remoteControl/status/changed`
95    RemoteControlStatusChanged(RemoteControlStatusChangedNotification),
96    /// `mcpServer/oauthLogin/completed`
97    McpServerOauthLoginCompleted(McpServerOauthLoginCompletedNotification),
98    /// `item/fileChange/patchUpdated`
99    FileChangePatchUpdated(FileChangePatchUpdatedNotification),
100    /// `item/plan/delta` (EXPERIMENTAL)
101    PlanDelta(PlanDeltaNotification),
102    /// `turn/plan/updated`
103    TurnPlanUpdated(TurnPlanUpdatedNotification),
104    /// `turn/diff/updated`
105    TurnDiffUpdated(TurnDiffUpdatedNotification),
106    /// `item/reasoning/summaryPartAdded`
107    ReasoningSummaryPartAdded(ReasoningSummaryPartAddedNotification),
108    /// `item/reasoning/textDelta`
109    ReasoningTextDelta(ReasoningTextDeltaNotification),
110    /// `account/login/completed`
111    AccountLoginCompleted(AccountLoginCompletedNotification),
112    /// `deprecationNotice`
113    DeprecationNotice(DeprecationNoticeNotification),
114    /// `guardianWarning`
115    GuardianWarning(GuardianWarningNotification),
116    /// `warning`
117    Warning(WarningNotification),
118    /// `thread/archived`
119    ThreadArchived(ThreadArchivedNotification),
120    /// `thread/closed`
121    ThreadClosed(ThreadClosedNotification),
122    /// `thread/unarchived`
123    ThreadUnarchived(ThreadUnarchivedNotification),
124    /// `thread/goal/cleared`
125    ThreadGoalCleared(ThreadGoalClearedNotification),
126    /// `thread/name/updated`
127    ThreadNameUpdated(ThreadNameUpdatedNotification),
128    /// `skills/changed`
129    SkillsChanged(SkillsChangedNotification),
130    /// `fs/changed`
131    FsChanged(FsChangedNotification),
132    /// `configWarning`
133    ConfigWarning(ConfigWarningNotification),
134    /// `account/updated`
135    AccountUpdated(AccountUpdatedNotification),
136    /// `app/list/updated`
137    AppListUpdated(AppListUpdatedNotification),
138    /// `command/exec/outputDelta`
139    CommandExecOutputDelta(CommandExecOutputDeltaNotification),
140    /// `externalAgentConfig/import/completed`
141    ExternalAgentConfigImportCompleted(ExternalAgentConfigImportCompletedNotification),
142    /// `fuzzyFileSearch/sessionCompleted`
143    FuzzyFileSearchSessionCompleted(FuzzyFileSearchSessionCompletedNotification),
144    /// `fuzzyFileSearch/sessionUpdated`
145    FuzzyFileSearchSessionUpdated(FuzzyFileSearchSessionUpdatedNotification),
146    /// `hook/completed`
147    HookCompleted(HookCompletedNotification),
148    /// `hook/started`
149    HookStarted(HookStartedNotification),
150    /// `item/autoApprovalReview/completed`
151    ItemGuardianApprovalReviewCompleted(ItemGuardianApprovalReviewCompletedNotification),
152    /// `item/autoApprovalReview/started`
153    ItemGuardianApprovalReviewStarted(ItemGuardianApprovalReviewStartedNotification),
154    /// `item/commandExecution/terminalInteraction`
155    TerminalInteraction(TerminalInteractionNotification),
156    /// `item/mcpToolCall/progress`
157    McpToolCallProgress(McpToolCallProgressNotification),
158    /// `model/rerouted`
159    ModelRerouted(ModelReroutedNotification),
160    /// `model/verification`
161    ModelVerification(ModelVerificationNotification),
162    /// `process/exited`
163    ProcessExited(ProcessExitedNotification),
164    /// `process/outputDelta`
165    ProcessOutputDelta(ProcessOutputDeltaNotification),
166    /// `serverRequest/resolved`
167    ServerRequestResolved(ServerRequestResolvedNotification),
168    /// `thread/compacted`
169    ContextCompacted(ContextCompactedNotification),
170    /// `thread/goal/updated`
171    ThreadGoalUpdated(ThreadGoalUpdatedNotification),
172    /// `thread/realtime/closed`
173    ThreadRealtimeClosed(ThreadRealtimeClosedNotification),
174    /// `thread/realtime/error`
175    ThreadRealtimeError(ThreadRealtimeErrorNotification),
176    /// `thread/realtime/itemAdded`
177    ThreadRealtimeItemAdded(ThreadRealtimeItemAddedNotification),
178    /// `thread/realtime/outputAudio/delta`
179    ThreadRealtimeOutputAudioDelta(ThreadRealtimeOutputAudioDeltaNotification),
180    /// `thread/realtime/sdp`
181    ThreadRealtimeSdp(ThreadRealtimeSdpNotification),
182    /// `thread/realtime/started`
183    ThreadRealtimeStarted(ThreadRealtimeStartedNotification),
184    /// `thread/realtime/transcript/delta`
185    ThreadRealtimeTranscriptDelta(ThreadRealtimeTranscriptDeltaNotification),
186    /// `thread/realtime/transcript/done`
187    ThreadRealtimeTranscriptDone(ThreadRealtimeTranscriptDoneNotification),
188    /// `windows/worldWritableWarning`
189    WindowsWorldWritableWarning(WindowsWorldWritableWarningNotification),
190    /// `windowsSandbox/setupCompleted`
191    WindowsSandboxSetupCompleted(WindowsSandboxSetupCompletedNotification),
192    /// A method this crate version does not yet model. The raw params are
193    /// preserved for caller inspection. Encountering this typically means
194    /// the installed codex CLI is newer than the bindings.
195    Unknown {
196        method: String,
197        params: Option<Value>,
198    },
199}
200
201impl Notification {
202    /// Return the wire `method` string for this notification.
203    pub fn method(&self) -> &str {
204        match self {
205            Self::ThreadStarted(_) => methods::THREAD_STARTED,
206            Self::ThreadStatusChanged(_) => methods::THREAD_STATUS_CHANGED,
207            Self::ThreadTokenUsageUpdated(_) => methods::THREAD_TOKEN_USAGE_UPDATED,
208            Self::TurnStarted(_) => methods::TURN_STARTED,
209            Self::TurnCompleted(_) => methods::TURN_COMPLETED,
210            Self::ItemStarted(_) => methods::ITEM_STARTED,
211            Self::ItemCompleted(_) => methods::ITEM_COMPLETED,
212            Self::AgentMessageDelta(_) => methods::AGENT_MESSAGE_DELTA,
213            Self::CmdOutputDelta(_) => methods::CMD_OUTPUT_DELTA,
214            Self::FileChangeOutputDelta(_) => methods::FILE_CHANGE_OUTPUT_DELTA,
215            Self::ReasoningDelta(_) => methods::REASONING_DELTA,
216            Self::Error(_) => methods::ERROR,
217            Self::AccountRateLimitsUpdated(_) => methods::ACCOUNT_RATE_LIMITS_UPDATED,
218            Self::McpServerStartupStatusUpdated(_) => methods::MCP_SERVER_STARTUP_STATUS_UPDATED,
219            Self::RemoteControlStatusChanged(_) => methods::REMOTE_CONTROL_STATUS_CHANGED,
220            Self::McpServerOauthLoginCompleted(_) => methods::MCP_SERVER_OAUTH_LOGIN_COMPLETED,
221            Self::FileChangePatchUpdated(_) => methods::FILE_CHANGE_PATCH_UPDATED,
222            Self::PlanDelta(_) => methods::PLAN_DELTA,
223            Self::TurnPlanUpdated(_) => methods::TURN_PLAN_UPDATED,
224            Self::TurnDiffUpdated(_) => methods::TURN_DIFF_UPDATED,
225            Self::ReasoningSummaryPartAdded(_) => methods::REASONING_SUMMARY_PART_ADDED,
226            Self::ReasoningTextDelta(_) => methods::REASONING_TEXT_DELTA,
227            Self::AccountLoginCompleted(_) => methods::ACCOUNT_LOGIN_COMPLETED,
228            Self::DeprecationNotice(_) => methods::DEPRECATION_NOTICE,
229            Self::GuardianWarning(_) => methods::GUARDIAN_WARNING,
230            Self::Warning(_) => methods::WARNING,
231            Self::ThreadArchived(_) => methods::THREAD_ARCHIVED,
232            Self::ThreadClosed(_) => methods::THREAD_CLOSED,
233            Self::ThreadUnarchived(_) => methods::THREAD_UNARCHIVED,
234            Self::ThreadGoalCleared(_) => methods::THREAD_GOAL_CLEARED,
235            Self::ThreadNameUpdated(_) => methods::THREAD_NAME_UPDATED,
236            Self::SkillsChanged(_) => methods::SKILLS_CHANGED,
237            Self::FsChanged(_) => methods::FS_CHANGED,
238            Self::ConfigWarning(_) => methods::CONFIG_WARNING,
239            Self::AccountUpdated(_) => methods::ACCOUNT_UPDATED,
240            Self::AppListUpdated(_) => methods::APP_LIST_UPDATED,
241            Self::CommandExecOutputDelta(_) => methods::COMMAND_EXEC_OUTPUT_DELTA,
242            Self::ExternalAgentConfigImportCompleted(_) => {
243                methods::EXTERNAL_AGENT_CONFIG_IMPORT_COMPLETED
244            }
245            Self::FuzzyFileSearchSessionCompleted(_) => {
246                methods::FUZZY_FILE_SEARCH_SESSION_COMPLETED
247            }
248            Self::FuzzyFileSearchSessionUpdated(_) => methods::FUZZY_FILE_SEARCH_SESSION_UPDATED,
249            Self::HookCompleted(_) => methods::HOOK_COMPLETED,
250            Self::HookStarted(_) => methods::HOOK_STARTED,
251            Self::ItemGuardianApprovalReviewCompleted(_) => {
252                methods::ITEM_AUTO_APPROVAL_REVIEW_COMPLETED
253            }
254            Self::ItemGuardianApprovalReviewStarted(_) => {
255                methods::ITEM_AUTO_APPROVAL_REVIEW_STARTED
256            }
257            Self::TerminalInteraction(_) => methods::ITEM_COMMAND_EXEC_TERMINAL_INTERACTION,
258            Self::McpToolCallProgress(_) => methods::ITEM_MCP_TOOL_CALL_PROGRESS,
259            Self::ModelRerouted(_) => methods::MODEL_REROUTED,
260            Self::ModelVerification(_) => methods::MODEL_VERIFICATION,
261            Self::ProcessExited(_) => methods::PROCESS_EXITED,
262            Self::ProcessOutputDelta(_) => methods::PROCESS_OUTPUT_DELTA,
263            Self::ServerRequestResolved(_) => methods::SERVER_REQUEST_RESOLVED,
264            Self::ContextCompacted(_) => methods::THREAD_COMPACTED,
265            Self::ThreadGoalUpdated(_) => methods::THREAD_GOAL_UPDATED,
266            Self::ThreadRealtimeClosed(_) => methods::THREAD_REALTIME_CLOSED,
267            Self::ThreadRealtimeError(_) => methods::THREAD_REALTIME_ERROR,
268            Self::ThreadRealtimeItemAdded(_) => methods::THREAD_REALTIME_ITEM_ADDED,
269            Self::ThreadRealtimeOutputAudioDelta(_) => methods::THREAD_REALTIME_OUTPUT_AUDIO_DELTA,
270            Self::ThreadRealtimeSdp(_) => methods::THREAD_REALTIME_SDP,
271            Self::ThreadRealtimeStarted(_) => methods::THREAD_REALTIME_STARTED,
272            Self::ThreadRealtimeTranscriptDelta(_) => methods::THREAD_REALTIME_TRANSCRIPT_DELTA,
273            Self::ThreadRealtimeTranscriptDone(_) => methods::THREAD_REALTIME_TRANSCRIPT_DONE,
274            Self::WindowsWorldWritableWarning(_) => methods::WINDOWS_WORLD_WRITABLE_WARNING,
275            Self::WindowsSandboxSetupCompleted(_) => methods::WINDOWS_SANDBOX_SETUP_COMPLETED,
276            Self::Unknown { method, .. } => method,
277        }
278    }
279
280    /// `true` if this notification's method isn't modeled by the crate.
281    pub fn is_unknown(&self) -> bool {
282        matches!(self, Self::Unknown { .. })
283    }
284
285    /// Construct a [`Notification`] from a `method` + `params` envelope.
286    ///
287    /// Returns an error if `method` is recognized but `params` doesn't
288    /// deserialize into the typed struct. Unknown methods route to
289    /// [`Notification::Unknown`] without error.
290    pub fn from_envelope(method: &str, params: Option<Value>) -> Result<Self, serde_json::Error> {
291        let params_value = params.clone().unwrap_or(Value::Null);
292        match method {
293            methods::THREAD_STARTED => {
294                serde_json::from_value(params_value).map(Self::ThreadStarted)
295            }
296            methods::THREAD_STATUS_CHANGED => {
297                serde_json::from_value(params_value).map(Self::ThreadStatusChanged)
298            }
299            methods::THREAD_TOKEN_USAGE_UPDATED => {
300                serde_json::from_value(params_value).map(Self::ThreadTokenUsageUpdated)
301            }
302            methods::TURN_STARTED => serde_json::from_value(params_value).map(Self::TurnStarted),
303            methods::TURN_COMPLETED => {
304                serde_json::from_value(params_value).map(Self::TurnCompleted)
305            }
306            methods::ITEM_STARTED => serde_json::from_value(params_value).map(Self::ItemStarted),
307            methods::ITEM_COMPLETED => {
308                serde_json::from_value(params_value).map(Self::ItemCompleted)
309            }
310            methods::AGENT_MESSAGE_DELTA => {
311                serde_json::from_value(params_value).map(Self::AgentMessageDelta)
312            }
313            methods::CMD_OUTPUT_DELTA => {
314                serde_json::from_value(params_value).map(Self::CmdOutputDelta)
315            }
316            methods::FILE_CHANGE_OUTPUT_DELTA => {
317                serde_json::from_value(params_value).map(Self::FileChangeOutputDelta)
318            }
319            methods::REASONING_DELTA => {
320                serde_json::from_value(params_value).map(Self::ReasoningDelta)
321            }
322            methods::ERROR => serde_json::from_value(params_value).map(Self::Error),
323            methods::ACCOUNT_RATE_LIMITS_UPDATED => {
324                serde_json::from_value(params_value).map(Self::AccountRateLimitsUpdated)
325            }
326            methods::MCP_SERVER_STARTUP_STATUS_UPDATED => {
327                serde_json::from_value(params_value).map(Self::McpServerStartupStatusUpdated)
328            }
329            methods::REMOTE_CONTROL_STATUS_CHANGED => {
330                serde_json::from_value(params_value).map(Self::RemoteControlStatusChanged)
331            }
332            methods::MCP_SERVER_OAUTH_LOGIN_COMPLETED => {
333                serde_json::from_value(params_value).map(Self::McpServerOauthLoginCompleted)
334            }
335            methods::FILE_CHANGE_PATCH_UPDATED => {
336                serde_json::from_value(params_value).map(Self::FileChangePatchUpdated)
337            }
338            methods::PLAN_DELTA => serde_json::from_value(params_value).map(Self::PlanDelta),
339            methods::TURN_PLAN_UPDATED => {
340                serde_json::from_value(params_value).map(Self::TurnPlanUpdated)
341            }
342            methods::TURN_DIFF_UPDATED => {
343                serde_json::from_value(params_value).map(Self::TurnDiffUpdated)
344            }
345            methods::REASONING_SUMMARY_PART_ADDED => {
346                serde_json::from_value(params_value).map(Self::ReasoningSummaryPartAdded)
347            }
348            methods::REASONING_TEXT_DELTA => {
349                serde_json::from_value(params_value).map(Self::ReasoningTextDelta)
350            }
351            methods::ACCOUNT_LOGIN_COMPLETED => {
352                serde_json::from_value(params_value).map(Self::AccountLoginCompleted)
353            }
354            methods::DEPRECATION_NOTICE => {
355                serde_json::from_value(params_value).map(Self::DeprecationNotice)
356            }
357            methods::GUARDIAN_WARNING => {
358                serde_json::from_value(params_value).map(Self::GuardianWarning)
359            }
360            methods::WARNING => serde_json::from_value(params_value).map(Self::Warning),
361            methods::THREAD_ARCHIVED => {
362                serde_json::from_value(params_value).map(Self::ThreadArchived)
363            }
364            methods::THREAD_CLOSED => serde_json::from_value(params_value).map(Self::ThreadClosed),
365            methods::THREAD_UNARCHIVED => {
366                serde_json::from_value(params_value).map(Self::ThreadUnarchived)
367            }
368            methods::THREAD_GOAL_CLEARED => {
369                serde_json::from_value(params_value).map(Self::ThreadGoalCleared)
370            }
371            methods::THREAD_NAME_UPDATED => {
372                serde_json::from_value(params_value).map(Self::ThreadNameUpdated)
373            }
374            methods::SKILLS_CHANGED => {
375                serde_json::from_value(params_value).map(Self::SkillsChanged)
376            }
377            methods::FS_CHANGED => serde_json::from_value(params_value).map(Self::FsChanged),
378            methods::CONFIG_WARNING => {
379                serde_json::from_value(params_value).map(Self::ConfigWarning)
380            }
381            methods::ACCOUNT_UPDATED => {
382                serde_json::from_value(params_value).map(Self::AccountUpdated)
383            }
384            methods::APP_LIST_UPDATED => {
385                serde_json::from_value(params_value).map(Self::AppListUpdated)
386            }
387            methods::COMMAND_EXEC_OUTPUT_DELTA => {
388                serde_json::from_value(params_value).map(Self::CommandExecOutputDelta)
389            }
390            methods::EXTERNAL_AGENT_CONFIG_IMPORT_COMPLETED => {
391                serde_json::from_value(params_value).map(Self::ExternalAgentConfigImportCompleted)
392            }
393            methods::FUZZY_FILE_SEARCH_SESSION_COMPLETED => {
394                serde_json::from_value(params_value).map(Self::FuzzyFileSearchSessionCompleted)
395            }
396            methods::FUZZY_FILE_SEARCH_SESSION_UPDATED => {
397                serde_json::from_value(params_value).map(Self::FuzzyFileSearchSessionUpdated)
398            }
399            methods::HOOK_COMPLETED => {
400                serde_json::from_value(params_value).map(Self::HookCompleted)
401            }
402            methods::HOOK_STARTED => serde_json::from_value(params_value).map(Self::HookStarted),
403            methods::ITEM_AUTO_APPROVAL_REVIEW_COMPLETED => {
404                serde_json::from_value(params_value).map(Self::ItemGuardianApprovalReviewCompleted)
405            }
406            methods::ITEM_AUTO_APPROVAL_REVIEW_STARTED => {
407                serde_json::from_value(params_value).map(Self::ItemGuardianApprovalReviewStarted)
408            }
409            methods::ITEM_COMMAND_EXEC_TERMINAL_INTERACTION => {
410                serde_json::from_value(params_value).map(Self::TerminalInteraction)
411            }
412            methods::ITEM_MCP_TOOL_CALL_PROGRESS => {
413                serde_json::from_value(params_value).map(Self::McpToolCallProgress)
414            }
415            methods::MODEL_REROUTED => {
416                serde_json::from_value(params_value).map(Self::ModelRerouted)
417            }
418            methods::MODEL_VERIFICATION => {
419                serde_json::from_value(params_value).map(Self::ModelVerification)
420            }
421            methods::PROCESS_EXITED => {
422                serde_json::from_value(params_value).map(Self::ProcessExited)
423            }
424            methods::PROCESS_OUTPUT_DELTA => {
425                serde_json::from_value(params_value).map(Self::ProcessOutputDelta)
426            }
427            methods::SERVER_REQUEST_RESOLVED => {
428                serde_json::from_value(params_value).map(Self::ServerRequestResolved)
429            }
430            methods::THREAD_COMPACTED => {
431                serde_json::from_value(params_value).map(Self::ContextCompacted)
432            }
433            methods::THREAD_GOAL_UPDATED => {
434                serde_json::from_value(params_value).map(Self::ThreadGoalUpdated)
435            }
436            methods::THREAD_REALTIME_CLOSED => {
437                serde_json::from_value(params_value).map(Self::ThreadRealtimeClosed)
438            }
439            methods::THREAD_REALTIME_ERROR => {
440                serde_json::from_value(params_value).map(Self::ThreadRealtimeError)
441            }
442            methods::THREAD_REALTIME_ITEM_ADDED => {
443                serde_json::from_value(params_value).map(Self::ThreadRealtimeItemAdded)
444            }
445            methods::THREAD_REALTIME_OUTPUT_AUDIO_DELTA => {
446                serde_json::from_value(params_value).map(Self::ThreadRealtimeOutputAudioDelta)
447            }
448            methods::THREAD_REALTIME_SDP => {
449                serde_json::from_value(params_value).map(Self::ThreadRealtimeSdp)
450            }
451            methods::THREAD_REALTIME_STARTED => {
452                serde_json::from_value(params_value).map(Self::ThreadRealtimeStarted)
453            }
454            methods::THREAD_REALTIME_TRANSCRIPT_DELTA => {
455                serde_json::from_value(params_value).map(Self::ThreadRealtimeTranscriptDelta)
456            }
457            methods::THREAD_REALTIME_TRANSCRIPT_DONE => {
458                serde_json::from_value(params_value).map(Self::ThreadRealtimeTranscriptDone)
459            }
460            methods::WINDOWS_WORLD_WRITABLE_WARNING => {
461                serde_json::from_value(params_value).map(Self::WindowsWorldWritableWarning)
462            }
463            methods::WINDOWS_SANDBOX_SETUP_COMPLETED => {
464                serde_json::from_value(params_value).map(Self::WindowsSandboxSetupCompleted)
465            }
466            _ => Ok(Self::Unknown {
467                method: method.to_string(),
468                params,
469            }),
470        }
471    }
472
473    /// Decompose this notification back into a `(method, params)` pair.
474    pub fn into_envelope(self) -> Result<(String, Option<Value>), serde_json::Error> {
475        fn pack<T: Serialize>(
476            method: &str,
477            v: &T,
478        ) -> Result<(String, Option<Value>), serde_json::Error> {
479            Ok((method.to_string(), Some(serde_json::to_value(v)?)))
480        }
481        match &self {
482            Self::ThreadStarted(v) => pack(methods::THREAD_STARTED, v),
483            Self::ThreadStatusChanged(v) => pack(methods::THREAD_STATUS_CHANGED, v),
484            Self::ThreadTokenUsageUpdated(v) => pack(methods::THREAD_TOKEN_USAGE_UPDATED, v),
485            Self::TurnStarted(v) => pack(methods::TURN_STARTED, v),
486            Self::TurnCompleted(v) => pack(methods::TURN_COMPLETED, v),
487            Self::ItemStarted(v) => pack(methods::ITEM_STARTED, v),
488            Self::ItemCompleted(v) => pack(methods::ITEM_COMPLETED, v),
489            Self::AgentMessageDelta(v) => pack(methods::AGENT_MESSAGE_DELTA, v),
490            Self::CmdOutputDelta(v) => pack(methods::CMD_OUTPUT_DELTA, v),
491            Self::FileChangeOutputDelta(v) => pack(methods::FILE_CHANGE_OUTPUT_DELTA, v),
492            Self::ReasoningDelta(v) => pack(methods::REASONING_DELTA, v),
493            Self::Error(v) => pack(methods::ERROR, v),
494            Self::AccountRateLimitsUpdated(v) => pack(methods::ACCOUNT_RATE_LIMITS_UPDATED, v),
495            Self::McpServerStartupStatusUpdated(v) => {
496                pack(methods::MCP_SERVER_STARTUP_STATUS_UPDATED, v)
497            }
498            Self::RemoteControlStatusChanged(v) => pack(methods::REMOTE_CONTROL_STATUS_CHANGED, v),
499            Self::McpServerOauthLoginCompleted(v) => {
500                pack(methods::MCP_SERVER_OAUTH_LOGIN_COMPLETED, v)
501            }
502            Self::FileChangePatchUpdated(v) => pack(methods::FILE_CHANGE_PATCH_UPDATED, v),
503            Self::PlanDelta(v) => pack(methods::PLAN_DELTA, v),
504            Self::TurnPlanUpdated(v) => pack(methods::TURN_PLAN_UPDATED, v),
505            Self::TurnDiffUpdated(v) => pack(methods::TURN_DIFF_UPDATED, v),
506            Self::ReasoningSummaryPartAdded(v) => pack(methods::REASONING_SUMMARY_PART_ADDED, v),
507            Self::ReasoningTextDelta(v) => pack(methods::REASONING_TEXT_DELTA, v),
508            Self::AccountLoginCompleted(v) => pack(methods::ACCOUNT_LOGIN_COMPLETED, v),
509            Self::DeprecationNotice(v) => pack(methods::DEPRECATION_NOTICE, v),
510            Self::GuardianWarning(v) => pack(methods::GUARDIAN_WARNING, v),
511            Self::Warning(v) => pack(methods::WARNING, v),
512            Self::ThreadArchived(v) => pack(methods::THREAD_ARCHIVED, v),
513            Self::ThreadClosed(v) => pack(methods::THREAD_CLOSED, v),
514            Self::ThreadUnarchived(v) => pack(methods::THREAD_UNARCHIVED, v),
515            Self::ThreadGoalCleared(v) => pack(methods::THREAD_GOAL_CLEARED, v),
516            Self::ThreadNameUpdated(v) => pack(methods::THREAD_NAME_UPDATED, v),
517            Self::SkillsChanged(v) => pack(methods::SKILLS_CHANGED, v),
518            Self::FsChanged(v) => pack(methods::FS_CHANGED, v),
519            Self::ConfigWarning(v) => pack(methods::CONFIG_WARNING, v),
520            Self::AccountUpdated(v) => pack(methods::ACCOUNT_UPDATED, v),
521            Self::AppListUpdated(v) => pack(methods::APP_LIST_UPDATED, v),
522            Self::CommandExecOutputDelta(v) => pack(methods::COMMAND_EXEC_OUTPUT_DELTA, v),
523            Self::ExternalAgentConfigImportCompleted(v) => {
524                pack(methods::EXTERNAL_AGENT_CONFIG_IMPORT_COMPLETED, v)
525            }
526            Self::FuzzyFileSearchSessionCompleted(v) => {
527                pack(methods::FUZZY_FILE_SEARCH_SESSION_COMPLETED, v)
528            }
529            Self::FuzzyFileSearchSessionUpdated(v) => {
530                pack(methods::FUZZY_FILE_SEARCH_SESSION_UPDATED, v)
531            }
532            Self::HookCompleted(v) => pack(methods::HOOK_COMPLETED, v),
533            Self::HookStarted(v) => pack(methods::HOOK_STARTED, v),
534            Self::ItemGuardianApprovalReviewCompleted(v) => {
535                pack(methods::ITEM_AUTO_APPROVAL_REVIEW_COMPLETED, v)
536            }
537            Self::ItemGuardianApprovalReviewStarted(v) => {
538                pack(methods::ITEM_AUTO_APPROVAL_REVIEW_STARTED, v)
539            }
540            Self::TerminalInteraction(v) => {
541                pack(methods::ITEM_COMMAND_EXEC_TERMINAL_INTERACTION, v)
542            }
543            Self::McpToolCallProgress(v) => pack(methods::ITEM_MCP_TOOL_CALL_PROGRESS, v),
544            Self::ModelRerouted(v) => pack(methods::MODEL_REROUTED, v),
545            Self::ModelVerification(v) => pack(methods::MODEL_VERIFICATION, v),
546            Self::ProcessExited(v) => pack(methods::PROCESS_EXITED, v),
547            Self::ProcessOutputDelta(v) => pack(methods::PROCESS_OUTPUT_DELTA, v),
548            Self::ServerRequestResolved(v) => pack(methods::SERVER_REQUEST_RESOLVED, v),
549            Self::ContextCompacted(v) => pack(methods::THREAD_COMPACTED, v),
550            Self::ThreadGoalUpdated(v) => pack(methods::THREAD_GOAL_UPDATED, v),
551            Self::ThreadRealtimeClosed(v) => pack(methods::THREAD_REALTIME_CLOSED, v),
552            Self::ThreadRealtimeError(v) => pack(methods::THREAD_REALTIME_ERROR, v),
553            Self::ThreadRealtimeItemAdded(v) => pack(methods::THREAD_REALTIME_ITEM_ADDED, v),
554            Self::ThreadRealtimeOutputAudioDelta(v) => {
555                pack(methods::THREAD_REALTIME_OUTPUT_AUDIO_DELTA, v)
556            }
557            Self::ThreadRealtimeSdp(v) => pack(methods::THREAD_REALTIME_SDP, v),
558            Self::ThreadRealtimeStarted(v) => pack(methods::THREAD_REALTIME_STARTED, v),
559            Self::ThreadRealtimeTranscriptDelta(v) => {
560                pack(methods::THREAD_REALTIME_TRANSCRIPT_DELTA, v)
561            }
562            Self::ThreadRealtimeTranscriptDone(v) => {
563                pack(methods::THREAD_REALTIME_TRANSCRIPT_DONE, v)
564            }
565            Self::WindowsWorldWritableWarning(v) => {
566                pack(methods::WINDOWS_WORLD_WRITABLE_WARNING, v)
567            }
568            Self::WindowsSandboxSetupCompleted(v) => {
569                pack(methods::WINDOWS_SANDBOX_SETUP_COMPLETED, v)
570            }
571            Self::Unknown { method, params } => Ok((method.clone(), params.clone())),
572        }
573    }
574}
575
576impl Serialize for Notification {
577    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
578        let (method, params) = self
579            .clone()
580            .into_envelope()
581            .map_err(serde::ser::Error::custom)?;
582        let mut env = serde_json::Map::new();
583        env.insert("method".to_string(), Value::String(method));
584        if let Some(p) = params {
585            env.insert("params".to_string(), p);
586        }
587        Value::Object(env).serialize(serializer)
588    }
589}
590
591impl<'de> Deserialize<'de> for Notification {
592    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
593        let value = Value::deserialize(deserializer)?;
594        let method = value
595            .get("method")
596            .and_then(|v| v.as_str())
597            .ok_or_else(|| serde::de::Error::missing_field("method"))?
598            .to_string();
599        let params = value.get("params").cloned();
600        Self::from_envelope(&method, params).map_err(serde::de::Error::custom)
601    }
602}
603
604/// A server-to-client request that requires a response (approval flow).
605///
606/// The wire envelope carries an `id` for response correlation; that `id` is
607/// held alongside this enum in [`ServerMessage::Request`] rather than embedded
608/// inside the variant, since responding doesn't depend on which approval-type
609/// was requested.
610#[derive(Debug, Clone)]
611pub enum ServerRequest {
612    /// `item/commandExecution/requestApproval`
613    CmdExecApproval(CommandExecutionRequestApprovalParams),
614    /// `item/fileChange/requestApproval`
615    FileChangeApproval(FileChangeRequestApprovalParams),
616    /// `item/tool/requestUserInput`
617    ToolRequestUserInput(crate::protocol::ToolRequestUserInputParams),
618    /// `mcpServer/elicitation/request`
619    McpServerElicitationRequest(crate::protocol::McpServerElicitationRequestParams),
620    /// `item/permissions/requestApproval`
621    PermissionsRequestApproval(crate::protocol::PermissionsRequestApprovalParams),
622    /// `item/tool/call`
623    ItemToolCall(crate::protocol::DynamicToolCallParams),
624    /// `account/chatgptAuthTokens/refresh`
625    ChatgptAuthTokensRefresh(crate::protocol::ChatgptAuthTokensRefreshParams),
626    /// `attestation/generate`
627    AttestationGenerate(crate::protocol::AttestationGenerateParams),
628    /// `applyPatchApproval`
629    ApplyPatchApproval(crate::protocol::ApplyPatchApprovalParams),
630    /// `execCommandApproval`
631    ExecCommandApproval(crate::protocol::ExecCommandApprovalParams),
632    /// A request method this crate version does not yet model.
633    Unknown {
634        method: String,
635        params: Option<Value>,
636    },
637}
638
639impl ServerRequest {
640    /// Return the wire `method` string for this request.
641    pub fn method(&self) -> &str {
642        match self {
643            Self::CmdExecApproval(_) => methods::CMD_EXEC_APPROVAL,
644            Self::FileChangeApproval(_) => methods::FILE_CHANGE_APPROVAL,
645            Self::ToolRequestUserInput(_) => methods::TOOL_REQUEST_USER_INPUT,
646            Self::McpServerElicitationRequest(_) => methods::MCP_SERVER_ELICITATION_REQUEST,
647            Self::PermissionsRequestApproval(_) => methods::PERMISSIONS_REQUEST_APPROVAL,
648            Self::ItemToolCall(_) => methods::ITEM_TOOL_CALL,
649            Self::ChatgptAuthTokensRefresh(_) => methods::CHATGPT_AUTH_TOKENS_REFRESH,
650            Self::AttestationGenerate(_) => methods::ATTESTATION_GENERATE,
651            Self::ApplyPatchApproval(_) => methods::APPLY_PATCH_APPROVAL,
652            Self::ExecCommandApproval(_) => methods::EXEC_COMMAND_APPROVAL,
653            Self::Unknown { method, .. } => method,
654        }
655    }
656
657    /// `true` if this request's method isn't modeled by the crate.
658    pub fn is_unknown(&self) -> bool {
659        matches!(self, Self::Unknown { .. })
660    }
661
662    /// Construct a [`ServerRequest`] from a `method` + `params` envelope.
663    pub fn from_envelope(method: &str, params: Option<Value>) -> Result<Self, serde_json::Error> {
664        let params_value = params.clone().unwrap_or(Value::Null);
665        match method {
666            methods::CMD_EXEC_APPROVAL => {
667                serde_json::from_value(params_value).map(Self::CmdExecApproval)
668            }
669            methods::FILE_CHANGE_APPROVAL => {
670                serde_json::from_value(params_value).map(Self::FileChangeApproval)
671            }
672            methods::TOOL_REQUEST_USER_INPUT => {
673                serde_json::from_value(params_value).map(Self::ToolRequestUserInput)
674            }
675            methods::MCP_SERVER_ELICITATION_REQUEST => {
676                serde_json::from_value(params_value).map(Self::McpServerElicitationRequest)
677            }
678            methods::PERMISSIONS_REQUEST_APPROVAL => {
679                serde_json::from_value(params_value).map(Self::PermissionsRequestApproval)
680            }
681            methods::ITEM_TOOL_CALL => serde_json::from_value(params_value).map(Self::ItemToolCall),
682            methods::CHATGPT_AUTH_TOKENS_REFRESH => {
683                serde_json::from_value(params_value).map(Self::ChatgptAuthTokensRefresh)
684            }
685            methods::ATTESTATION_GENERATE => {
686                serde_json::from_value(params_value).map(Self::AttestationGenerate)
687            }
688            methods::APPLY_PATCH_APPROVAL => {
689                serde_json::from_value(params_value).map(Self::ApplyPatchApproval)
690            }
691            methods::EXEC_COMMAND_APPROVAL => {
692                serde_json::from_value(params_value).map(Self::ExecCommandApproval)
693            }
694            _ => Ok(Self::Unknown {
695                method: method.to_string(),
696                params,
697            }),
698        }
699    }
700}
701
702/// A message coming from the app-server.
703///
704/// Replaces the previous loose `{ method, params }` shape with typed enums.
705/// Match on the outer variant first to distinguish notifications (no response)
706/// from requests (need [`crate::AsyncClient::respond`] /
707/// [`crate::SyncClient::respond`]).
708#[derive(Debug, Clone)]
709pub enum ServerMessage {
710    /// A notification — no response required.
711    Notification(Notification),
712    /// A request — call `respond(id, ...)` on the client with the matching id.
713    Request {
714        id: RequestId,
715        request: ServerRequest,
716    },
717}
718
719impl ServerMessage {
720    /// `true` if this message is an unmodeled method (notification or request).
721    pub fn is_unknown(&self) -> bool {
722        match self {
723            Self::Notification(n) => n.is_unknown(),
724            Self::Request { request, .. } => request.is_unknown(),
725        }
726    }
727}
728
729#[cfg(test)]
730mod tests {
731    use super::*;
732
733    #[test]
734    fn test_notification_unknown_method_routes_to_unknown_variant() {
735        let n = Notification::from_envelope("foo/bar", Some(serde_json::json!({"x": 1})))
736            .expect("unknown methods do not error");
737        match n {
738            Notification::Unknown { method, params } => {
739                assert_eq!(method, "foo/bar");
740                assert_eq!(params, Some(serde_json::json!({"x": 1})));
741            }
742            other => panic!("expected Unknown, got {:?}", other),
743        }
744    }
745
746    #[test]
747    fn test_notification_known_method_with_bad_params_errors() {
748        // thread/started expects a `thread` field — wrong shape should error.
749        let err = Notification::from_envelope("thread/started", Some(serde_json::json!({})));
750        assert!(err.is_err());
751    }
752
753    #[test]
754    fn test_notification_round_trip_envelope() {
755        let wire = serde_json::json!({
756            "method": "item/agentMessage/delta",
757            "params": {"threadId": "t1", "turnId": "u1", "itemId": "i1", "delta": "hi"},
758        });
759        let n: Notification = serde_json::from_value(wire.clone()).unwrap();
760        assert!(matches!(n, Notification::AgentMessageDelta(_)));
761        let back = serde_json::to_value(&n).unwrap();
762        assert_eq!(back, wire);
763    }
764}