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