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::error::{Error, ParseError};
27use crate::jsonrpc::{JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, RequestId};
28use crate::protocol::{
29    methods, AccountLoginCompletedNotification, AccountRateLimitsUpdatedNotification,
30    AccountUpdatedNotification, AgentMessageDeltaNotification, AppListUpdatedNotification,
31    AuthRecoveryNotification, CommandExecOutputDeltaNotification,
32    CommandExecutionOutputDeltaNotification, CommandExecutionRequestApprovalParams,
33    ConfigWarningNotification, ContextCompactedNotification, DeprecationNoticeNotification,
34    EnvironmentConnectionNotification, ErrorNotification,
35    ExternalAgentConfigImportCompletedNotification, ExternalAgentConfigImportProgressNotification,
36    FileChangeOutputDeltaNotification, FileChangePatchUpdatedNotification,
37    FileChangeRequestApprovalParams, FsChangedNotification,
38    FuzzyFileSearchSessionCompletedNotification, FuzzyFileSearchSessionUpdatedNotification,
39    GuardianWarningNotification, HookCompletedNotification, HookStartedNotification,
40    ItemCompletedNotification, ItemGuardianApprovalReviewCompletedNotification,
41    ItemGuardianApprovalReviewStartedNotification, ItemStartedNotification,
42    McpServerEventStreamNotification, McpServerOauthLoginCompletedNotification,
43    McpServerStatusUpdatedNotification, McpToolCallProgressNotification, ModelReroutedNotification,
44    ModelSafetyBufferingUpdatedNotification, ModelVerificationNotification, PlanDeltaNotification,
45    ProcessExitedNotification, ProcessOutputDeltaNotification, ProjectChangedNotification,
46    ReasoningSummaryPartAddedNotification, ReasoningSummaryTextDeltaNotification,
47    ReasoningTextDeltaNotification, RemoteControlStatusChangedNotification,
48    ServerRequestResolvedNotification, SkillsChangedNotification, StrictReviewRequiredNotification,
49    TerminalInteractionNotification, ThreadArchivedNotification, ThreadClosedNotification,
50    ThreadDeletedNotification, ThreadGoalClearedNotification, ThreadGoalUpdatedNotification,
51    ThreadNameUpdatedNotification, ThreadProjectUpdatedNotification,
52    ThreadQueueChangedNotification, ThreadRealtimeClosedNotification,
53    ThreadRealtimeErrorNotification, ThreadRealtimeItemAddedNotification,
54    ThreadRealtimeItemCompletedNotification, ThreadRealtimeItemStartedNotification,
55    ThreadRealtimeItemTranscriptDeltaNotification, ThreadRealtimeOutputAudioDeltaNotification,
56    ThreadRealtimeSdpNotification, ThreadRealtimeStartedNotification,
57    ThreadRealtimeTranscriptDeltaNotification, ThreadRealtimeTranscriptDoneNotification,
58    ThreadRevertedNotification, ThreadSettingsUpdatedNotification, ThreadStartedNotification,
59    ThreadStatusChangedNotification, ThreadTokenUsageUpdatedNotification,
60    ThreadUnarchivedNotification, TurnCompletedNotification, TurnDiffUpdatedNotification,
61    TurnModerationMetadataNotification, TurnPlanUpdatedNotification, TurnStartedNotification,
62    WarningNotification, WindowsSandboxSetupCompletedNotification,
63    WindowsWorldWritableWarningNotification,
64};
65use serde::{Deserialize, Deserializer, Serialize, Serializer};
66use serde_json::Value;
67
68/// A server-to-client notification.
69///
70/// Each variant maps to a single `method` string on the wire. The `Unknown`
71/// variant captures methods this crate version doesn't model yet, preserving
72/// the raw payload for inspection.
73#[derive(Debug, Clone)]
74pub enum Notification {
75    /// `thread/started`
76    ThreadStarted(ThreadStartedNotification),
77    /// `thread/status/changed`
78    ThreadStatusChanged(ThreadStatusChangedNotification),
79    /// `thread/tokenUsage/updated`
80    ThreadTokenUsageUpdated(ThreadTokenUsageUpdatedNotification),
81    /// `turn/started`
82    TurnStarted(TurnStartedNotification),
83    /// `turn/completed`
84    TurnCompleted(TurnCompletedNotification),
85    /// `item/started`
86    ItemStarted(ItemStartedNotification),
87    /// `item/completed`
88    ItemCompleted(ItemCompletedNotification),
89    /// `item/agentMessage/delta`
90    AgentMessageDelta(AgentMessageDeltaNotification),
91    /// `item/commandExecution/outputDelta`
92    CmdOutputDelta(CommandExecutionOutputDeltaNotification),
93    /// `item/fileChange/outputDelta`
94    FileChangeOutputDelta(FileChangeOutputDeltaNotification),
95    /// `item/reasoning/summaryTextDelta`
96    ReasoningDelta(ReasoningSummaryTextDeltaNotification),
97    /// `error`
98    Error(ErrorNotification),
99    /// `account/rateLimits/updated`
100    AccountRateLimitsUpdated(AccountRateLimitsUpdatedNotification),
101    /// `mcpServer/startupStatus/updated`
102    McpServerStartupStatusUpdated(McpServerStatusUpdatedNotification),
103    /// `remoteControl/status/changed`
104    RemoteControlStatusChanged(RemoteControlStatusChangedNotification),
105    /// `mcpServer/oauthLogin/completed`
106    McpServerOauthLoginCompleted(McpServerOauthLoginCompletedNotification),
107    /// `item/fileChange/patchUpdated`
108    FileChangePatchUpdated(FileChangePatchUpdatedNotification),
109    /// `item/plan/delta` (EXPERIMENTAL)
110    PlanDelta(PlanDeltaNotification),
111    /// `turn/plan/updated`
112    TurnPlanUpdated(TurnPlanUpdatedNotification),
113    /// `turn/diff/updated`
114    TurnDiffUpdated(TurnDiffUpdatedNotification),
115    /// `item/reasoning/summaryPartAdded`
116    ReasoningSummaryPartAdded(ReasoningSummaryPartAddedNotification),
117    /// `item/reasoning/textDelta`
118    ReasoningTextDelta(ReasoningTextDeltaNotification),
119    /// `account/login/completed`
120    AccountLoginCompleted(AccountLoginCompletedNotification),
121    /// `deprecationNotice`
122    DeprecationNotice(DeprecationNoticeNotification),
123    /// `guardianWarning`
124    GuardianWarning(GuardianWarningNotification),
125    /// `warning`
126    Warning(WarningNotification),
127    /// `thread/archived`
128    ThreadArchived(ThreadArchivedNotification),
129    /// `thread/closed`
130    ThreadClosed(ThreadClosedNotification),
131    /// `thread/deleted`
132    ThreadDeleted(ThreadDeletedNotification),
133    /// `thread/unarchived`
134    ThreadUnarchived(ThreadUnarchivedNotification),
135    /// `thread/goal/cleared`
136    ThreadGoalCleared(ThreadGoalClearedNotification),
137    /// `thread/name/updated`
138    ThreadNameUpdated(ThreadNameUpdatedNotification),
139    /// `skills/changed`
140    SkillsChanged(SkillsChangedNotification),
141    /// `fs/changed`
142    FsChanged(FsChangedNotification),
143    /// `configWarning`
144    ConfigWarning(ConfigWarningNotification),
145    /// `account/updated`
146    AccountUpdated(AccountUpdatedNotification),
147    /// `app/list/updated`
148    AppListUpdated(AppListUpdatedNotification),
149    /// `command/exec/outputDelta`
150    CommandExecOutputDelta(CommandExecOutputDeltaNotification),
151    /// `externalAgentConfig/import/completed`
152    ExternalAgentConfigImportCompleted(ExternalAgentConfigImportCompletedNotification),
153    /// `fuzzyFileSearch/sessionCompleted`
154    FuzzyFileSearchSessionCompleted(FuzzyFileSearchSessionCompletedNotification),
155    /// `fuzzyFileSearch/sessionUpdated`
156    FuzzyFileSearchSessionUpdated(FuzzyFileSearchSessionUpdatedNotification),
157    /// `hook/completed`
158    HookCompleted(HookCompletedNotification),
159    /// `hook/started`
160    HookStarted(HookStartedNotification),
161    /// `item/autoApprovalReview/completed`
162    ItemGuardianApprovalReviewCompleted(ItemGuardianApprovalReviewCompletedNotification),
163    /// `item/autoApprovalReview/started`
164    ItemGuardianApprovalReviewStarted(ItemGuardianApprovalReviewStartedNotification),
165    /// `item/commandExecution/terminalInteraction`
166    TerminalInteraction(TerminalInteractionNotification),
167    /// `item/mcpToolCall/progress`
168    McpToolCallProgress(McpToolCallProgressNotification),
169    /// `model/rerouted`
170    ModelRerouted(ModelReroutedNotification),
171    /// `model/verification`
172    ModelVerification(ModelVerificationNotification),
173    /// `process/exited`
174    ProcessExited(ProcessExitedNotification),
175    /// `process/outputDelta`
176    ProcessOutputDelta(ProcessOutputDeltaNotification),
177    /// `serverRequest/resolved`
178    ServerRequestResolved(ServerRequestResolvedNotification),
179    /// `thread/compacted`
180    ContextCompacted(ContextCompactedNotification),
181    /// `thread/goal/updated`
182    ThreadGoalUpdated(ThreadGoalUpdatedNotification),
183    /// `thread/realtime/closed`
184    ThreadRealtimeClosed(ThreadRealtimeClosedNotification),
185    /// `thread/realtime/error`
186    ThreadRealtimeError(ThreadRealtimeErrorNotification),
187    /// `thread/realtime/itemAdded`
188    ThreadRealtimeItemAdded(ThreadRealtimeItemAddedNotification),
189    /// `thread/realtime/outputAudio/delta`
190    ThreadRealtimeOutputAudioDelta(ThreadRealtimeOutputAudioDeltaNotification),
191    /// `thread/realtime/sdp`
192    ThreadRealtimeSdp(ThreadRealtimeSdpNotification),
193    /// `thread/realtime/started`
194    ThreadRealtimeStarted(ThreadRealtimeStartedNotification),
195    /// `thread/realtime/transcript/delta`
196    ThreadRealtimeTranscriptDelta(ThreadRealtimeTranscriptDeltaNotification),
197    /// `thread/realtime/transcript/done`
198    ThreadRealtimeTranscriptDone(ThreadRealtimeTranscriptDoneNotification),
199    /// `windows/worldWritableWarning`
200    WindowsWorldWritableWarning(WindowsWorldWritableWarningNotification),
201    /// `windowsSandbox/setupCompleted`
202    WindowsSandboxSetupCompleted(WindowsSandboxSetupCompletedNotification),
203    /// `thread/settings/updated`
204    ThreadSettingsUpdated(ThreadSettingsUpdatedNotification),
205    /// `turn/moderationMetadata`
206    TurnModerationMetadata(TurnModerationMetadataNotification),
207    /// `externalAgentConfig/import/progress`
208    ExternalAgentConfigImportProgress(ExternalAgentConfigImportProgressNotification),
209    /// `model/safetyBuffering/updated`
210    ModelSafetyBufferingUpdated(ModelSafetyBufferingUpdatedNotification),
211    /// `thread/environment/connected`
212    ThreadEnvironmentConnected(EnvironmentConnectionNotification),
213    /// `thread/environment/disconnected`
214    ThreadEnvironmentDisconnected(EnvironmentConnectionNotification),
215    /// `autoApprovalReview/strictReviewRequired` (0.147)
216    StrictReviewRequired(StrictReviewRequiredNotification),
217    /// `project/changed` (0.147)
218    ProjectChanged(ProjectChangedNotification),
219    /// `thread/project/updated` (0.147)
220    ThreadProjectUpdated(ThreadProjectUpdatedNotification),
221    /// `thread/queue/changed` (0.147)
222    ThreadQueueChanged(ThreadQueueChangedNotification),
223    /// `thread/reverted` (0.147)
224    ThreadReverted(ThreadRevertedNotification),
225    /// `mcpServer/event/stream/notification` (0.148 upstream)
226    McpServerEventStream(McpServerEventStreamNotification),
227    /// `thread/realtime/item/started` (0.148 upstream, experimental)
228    ThreadRealtimeItemStarted(ThreadRealtimeItemStartedNotification),
229    /// `thread/realtime/item/completed` (0.148 upstream, experimental)
230    ThreadRealtimeItemCompleted(ThreadRealtimeItemCompletedNotification),
231    /// `thread/realtime/item/transcript/delta` (0.148 upstream, experimental)
232    ThreadRealtimeItemTranscriptDelta(ThreadRealtimeItemTranscriptDeltaNotification),
233    /// `modelProvider/authRecoveryStarted` (0.148 upstream)
234    ModelProviderAuthRecoveryStarted(AuthRecoveryNotification),
235    /// `modelProvider/authRecoveryCompleted` (0.148 upstream)
236    ModelProviderAuthRecoveryCompleted(AuthRecoveryNotification),
237    /// A method this crate version does not yet model. The raw params are
238    /// preserved for caller inspection. Encountering this typically means
239    /// the installed codex CLI is newer than the bindings.
240    Unknown {
241        method: String,
242        params: Option<Value>,
243    },
244}
245
246impl Notification {
247    /// Return the wire `method` string for this notification.
248    pub fn method(&self) -> &str {
249        match self {
250            Self::ThreadStarted(_) => methods::THREAD_STARTED,
251            Self::StrictReviewRequired(_) => methods::STRICT_REVIEW_REQUIRED,
252            Self::ProjectChanged(_) => methods::PROJECT_CHANGED,
253            Self::ThreadProjectUpdated(_) => methods::THREAD_PROJECT_UPDATED,
254            Self::ThreadQueueChanged(_) => methods::THREAD_QUEUE_CHANGED,
255            Self::ThreadReverted(_) => methods::THREAD_REVERTED,
256            Self::McpServerEventStream(_) => methods::MCP_SERVER_EVENT_STREAM,
257            Self::ThreadRealtimeItemStarted(_) => methods::THREAD_REALTIME_ITEM_STARTED,
258            Self::ThreadRealtimeItemCompleted(_) => methods::THREAD_REALTIME_ITEM_COMPLETED,
259            Self::ThreadRealtimeItemTranscriptDelta(_) => {
260                methods::THREAD_REALTIME_ITEM_TRANSCRIPT_DELTA
261            }
262            Self::ModelProviderAuthRecoveryStarted(_) => {
263                methods::MODEL_PROVIDER_AUTH_RECOVERY_STARTED
264            }
265            Self::ModelProviderAuthRecoveryCompleted(_) => {
266                methods::MODEL_PROVIDER_AUTH_RECOVERY_COMPLETED
267            }
268            Self::ThreadStatusChanged(_) => methods::THREAD_STATUS_CHANGED,
269            Self::ThreadTokenUsageUpdated(_) => methods::THREAD_TOKEN_USAGE_UPDATED,
270            Self::TurnStarted(_) => methods::TURN_STARTED,
271            Self::TurnCompleted(_) => methods::TURN_COMPLETED,
272            Self::ItemStarted(_) => methods::ITEM_STARTED,
273            Self::ItemCompleted(_) => methods::ITEM_COMPLETED,
274            Self::AgentMessageDelta(_) => methods::AGENT_MESSAGE_DELTA,
275            Self::CmdOutputDelta(_) => methods::CMD_OUTPUT_DELTA,
276            Self::FileChangeOutputDelta(_) => methods::FILE_CHANGE_OUTPUT_DELTA,
277            Self::ReasoningDelta(_) => methods::REASONING_DELTA,
278            Self::Error(_) => methods::ERROR,
279            Self::AccountRateLimitsUpdated(_) => methods::ACCOUNT_RATE_LIMITS_UPDATED,
280            Self::McpServerStartupStatusUpdated(_) => methods::MCP_SERVER_STARTUP_STATUS_UPDATED,
281            Self::RemoteControlStatusChanged(_) => methods::REMOTE_CONTROL_STATUS_CHANGED,
282            Self::McpServerOauthLoginCompleted(_) => methods::MCP_SERVER_OAUTH_LOGIN_COMPLETED,
283            Self::FileChangePatchUpdated(_) => methods::FILE_CHANGE_PATCH_UPDATED,
284            Self::PlanDelta(_) => methods::PLAN_DELTA,
285            Self::TurnPlanUpdated(_) => methods::TURN_PLAN_UPDATED,
286            Self::TurnDiffUpdated(_) => methods::TURN_DIFF_UPDATED,
287            Self::ReasoningSummaryPartAdded(_) => methods::REASONING_SUMMARY_PART_ADDED,
288            Self::ReasoningTextDelta(_) => methods::REASONING_TEXT_DELTA,
289            Self::AccountLoginCompleted(_) => methods::ACCOUNT_LOGIN_COMPLETED,
290            Self::DeprecationNotice(_) => methods::DEPRECATION_NOTICE,
291            Self::GuardianWarning(_) => methods::GUARDIAN_WARNING,
292            Self::Warning(_) => methods::WARNING,
293            Self::ThreadArchived(_) => methods::THREAD_ARCHIVED,
294            Self::ThreadClosed(_) => methods::THREAD_CLOSED,
295            Self::ThreadDeleted(_) => methods::THREAD_DELETED,
296            Self::ThreadUnarchived(_) => methods::THREAD_UNARCHIVED,
297            Self::ThreadGoalCleared(_) => methods::THREAD_GOAL_CLEARED,
298            Self::ThreadNameUpdated(_) => methods::THREAD_NAME_UPDATED,
299            Self::SkillsChanged(_) => methods::SKILLS_CHANGED,
300            Self::FsChanged(_) => methods::FS_CHANGED,
301            Self::ConfigWarning(_) => methods::CONFIG_WARNING,
302            Self::AccountUpdated(_) => methods::ACCOUNT_UPDATED,
303            Self::AppListUpdated(_) => methods::APP_LIST_UPDATED,
304            Self::CommandExecOutputDelta(_) => methods::COMMAND_EXEC_OUTPUT_DELTA,
305            Self::ExternalAgentConfigImportCompleted(_) => {
306                methods::EXTERNAL_AGENT_CONFIG_IMPORT_COMPLETED
307            }
308            Self::FuzzyFileSearchSessionCompleted(_) => {
309                methods::FUZZY_FILE_SEARCH_SESSION_COMPLETED
310            }
311            Self::FuzzyFileSearchSessionUpdated(_) => methods::FUZZY_FILE_SEARCH_SESSION_UPDATED,
312            Self::HookCompleted(_) => methods::HOOK_COMPLETED,
313            Self::HookStarted(_) => methods::HOOK_STARTED,
314            Self::ItemGuardianApprovalReviewCompleted(_) => {
315                methods::ITEM_AUTO_APPROVAL_REVIEW_COMPLETED
316            }
317            Self::ItemGuardianApprovalReviewStarted(_) => {
318                methods::ITEM_AUTO_APPROVAL_REVIEW_STARTED
319            }
320            Self::TerminalInteraction(_) => methods::ITEM_COMMAND_EXEC_TERMINAL_INTERACTION,
321            Self::McpToolCallProgress(_) => methods::ITEM_MCP_TOOL_CALL_PROGRESS,
322            Self::ModelRerouted(_) => methods::MODEL_REROUTED,
323            Self::ModelVerification(_) => methods::MODEL_VERIFICATION,
324            Self::ProcessExited(_) => methods::PROCESS_EXITED,
325            Self::ProcessOutputDelta(_) => methods::PROCESS_OUTPUT_DELTA,
326            Self::ServerRequestResolved(_) => methods::SERVER_REQUEST_RESOLVED,
327            Self::ContextCompacted(_) => methods::THREAD_COMPACTED,
328            Self::ThreadGoalUpdated(_) => methods::THREAD_GOAL_UPDATED,
329            Self::ThreadRealtimeClosed(_) => methods::THREAD_REALTIME_CLOSED,
330            Self::ThreadRealtimeError(_) => methods::THREAD_REALTIME_ERROR,
331            Self::ThreadRealtimeItemAdded(_) => methods::THREAD_REALTIME_ITEM_ADDED,
332            Self::ThreadRealtimeOutputAudioDelta(_) => methods::THREAD_REALTIME_OUTPUT_AUDIO_DELTA,
333            Self::ThreadRealtimeSdp(_) => methods::THREAD_REALTIME_SDP,
334            Self::ThreadRealtimeStarted(_) => methods::THREAD_REALTIME_STARTED,
335            Self::ThreadRealtimeTranscriptDelta(_) => methods::THREAD_REALTIME_TRANSCRIPT_DELTA,
336            Self::ThreadRealtimeTranscriptDone(_) => methods::THREAD_REALTIME_TRANSCRIPT_DONE,
337            Self::WindowsWorldWritableWarning(_) => methods::WINDOWS_WORLD_WRITABLE_WARNING,
338            Self::WindowsSandboxSetupCompleted(_) => methods::WINDOWS_SANDBOX_SETUP_COMPLETED,
339            Self::ThreadSettingsUpdated(_) => methods::THREAD_SETTINGS_UPDATED,
340            Self::TurnModerationMetadata(_) => methods::TURN_MODERATION_METADATA,
341            Self::ExternalAgentConfigImportProgress(_) => {
342                methods::EXTERNAL_AGENT_CONFIG_IMPORT_PROGRESS
343            }
344            Self::ModelSafetyBufferingUpdated(_) => methods::MODEL_SAFETY_BUFFERING_UPDATED,
345            Self::ThreadEnvironmentConnected(_) => methods::THREAD_ENVIRONMENT_CONNECTED,
346            Self::ThreadEnvironmentDisconnected(_) => methods::THREAD_ENVIRONMENT_DISCONNECTED,
347            Self::Unknown { method, .. } => method,
348        }
349    }
350
351    /// `true` if this notification's method isn't modeled by the crate.
352    pub fn is_unknown(&self) -> bool {
353        matches!(self, Self::Unknown { .. })
354    }
355
356    /// Return the turn id this notification is scoped to, if it carries one.
357    ///
358    /// Reads the typed `turnId` field (or `turn.id` for the turn-lifecycle
359    /// notifications) directly, so callers don't have to round-trip through
360    /// [`into_envelope`](Self::into_envelope) and poke `serde_json::Value`
361    /// fields. Returns `None` for notifications that aren't turn-scoped, and
362    /// treats an empty id the server omitted as absent.
363    ///
364    /// The turn id from `turn/started` is what
365    /// [`turn/interrupt`](crate::AsyncClient::turn_interrupt) needs to cancel
366    /// the active turn.
367    pub fn turn_id(&self) -> Option<&str> {
368        let id = match self {
369            Self::TurnStarted(n) => n.turn.id.as_str(),
370            Self::TurnCompleted(n) => n.turn.id.as_str(),
371            Self::AgentMessageDelta(n) => n.turn_id.as_str(),
372            Self::CmdOutputDelta(n) => n.turn_id.as_str(),
373            Self::FileChangeOutputDelta(n) => n.turn_id.as_str(),
374            Self::ReasoningDelta(n) => n.turn_id.as_str(),
375            Self::Error(n) => n.turn_id.as_str(),
376            Self::FileChangePatchUpdated(n) => n.turn_id.as_str(),
377            Self::PlanDelta(n) => n.turn_id.as_str(),
378            Self::TurnPlanUpdated(n) => n.turn_id.as_str(),
379            Self::TurnDiffUpdated(n) => n.turn_id.as_str(),
380            Self::TurnModerationMetadata(n) => n.turn_id.as_str(),
381            Self::ReasoningSummaryPartAdded(n) => n.turn_id.as_str(),
382            Self::ReasoningTextDelta(n) => n.turn_id.as_str(),
383            Self::ItemStarted(n) => n.turn_id.as_str(),
384            Self::ItemCompleted(n) => n.turn_id.as_str(),
385            Self::ContextCompacted(n) => n.turn_id.as_str(),
386            Self::McpToolCallProgress(n) => n.turn_id.as_str(),
387            Self::ModelRerouted(n) => n.turn_id.as_str(),
388            Self::ModelVerification(n) => n.turn_id.as_str(),
389            Self::ModelSafetyBufferingUpdated(n) => n.turn_id.as_str(),
390            Self::TerminalInteraction(n) => n.turn_id.as_str(),
391            Self::ThreadTokenUsageUpdated(n) => n.turn_id.as_str(),
392            Self::ItemGuardianApprovalReviewStarted(n) => n.turn_id.as_str(),
393            Self::ItemGuardianApprovalReviewCompleted(n) => n.turn_id.as_str(),
394            _ => return None,
395        };
396        (!id.is_empty()).then_some(id)
397    }
398
399    /// Return the typed [`ThreadItem`](crate::protocol::ThreadItem) this
400    /// notification carries, for `item/started` and `item/completed`.
401    ///
402    /// Lets downstream event adapters read the item's typed fields instead of
403    /// reserializing it back into `serde_json::Value`.
404    pub fn thread_item(&self) -> Option<&crate::protocol::ThreadItem> {
405        match self {
406            Self::ItemStarted(n) => Some(&n.item),
407            Self::ItemCompleted(n) => Some(&n.item),
408            _ => None,
409        }
410    }
411
412    /// Construct a [`Notification`] from a `method` + `params` envelope.
413    ///
414    /// Returns an error if `method` is recognized but `params` doesn't
415    /// deserialize into the typed struct. Unknown methods route to
416    /// [`Notification::Unknown`] without error.
417    pub fn from_envelope(method: &str, params: Option<Value>) -> Result<Self, serde_json::Error> {
418        let params_value = params.clone().unwrap_or(Value::Null);
419        match method {
420            methods::THREAD_STARTED => {
421                serde_json::from_value(params_value).map(Self::ThreadStarted)
422            }
423            methods::STRICT_REVIEW_REQUIRED => {
424                serde_json::from_value(params_value).map(Self::StrictReviewRequired)
425            }
426            methods::PROJECT_CHANGED => {
427                serde_json::from_value(params_value).map(Self::ProjectChanged)
428            }
429            methods::THREAD_PROJECT_UPDATED => {
430                serde_json::from_value(params_value).map(Self::ThreadProjectUpdated)
431            }
432            methods::THREAD_QUEUE_CHANGED => {
433                serde_json::from_value(params_value).map(Self::ThreadQueueChanged)
434            }
435            methods::THREAD_REVERTED => {
436                serde_json::from_value(params_value).map(Self::ThreadReverted)
437            }
438            methods::MCP_SERVER_EVENT_STREAM => {
439                serde_json::from_value(params_value).map(Self::McpServerEventStream)
440            }
441            methods::THREAD_REALTIME_ITEM_STARTED => {
442                serde_json::from_value(params_value).map(Self::ThreadRealtimeItemStarted)
443            }
444            methods::THREAD_REALTIME_ITEM_COMPLETED => {
445                serde_json::from_value(params_value).map(Self::ThreadRealtimeItemCompleted)
446            }
447            methods::THREAD_REALTIME_ITEM_TRANSCRIPT_DELTA => {
448                serde_json::from_value(params_value).map(Self::ThreadRealtimeItemTranscriptDelta)
449            }
450            methods::MODEL_PROVIDER_AUTH_RECOVERY_STARTED => {
451                serde_json::from_value(params_value).map(Self::ModelProviderAuthRecoveryStarted)
452            }
453            methods::MODEL_PROVIDER_AUTH_RECOVERY_COMPLETED => {
454                serde_json::from_value(params_value).map(Self::ModelProviderAuthRecoveryCompleted)
455            }
456            methods::THREAD_STATUS_CHANGED => {
457                serde_json::from_value(params_value).map(Self::ThreadStatusChanged)
458            }
459            methods::THREAD_TOKEN_USAGE_UPDATED => {
460                serde_json::from_value(params_value).map(Self::ThreadTokenUsageUpdated)
461            }
462            methods::TURN_STARTED => serde_json::from_value(params_value).map(Self::TurnStarted),
463            methods::TURN_COMPLETED => {
464                serde_json::from_value(params_value).map(Self::TurnCompleted)
465            }
466            methods::ITEM_STARTED => serde_json::from_value(params_value).map(Self::ItemStarted),
467            methods::ITEM_COMPLETED => {
468                serde_json::from_value(params_value).map(Self::ItemCompleted)
469            }
470            methods::AGENT_MESSAGE_DELTA => {
471                serde_json::from_value(params_value).map(Self::AgentMessageDelta)
472            }
473            methods::CMD_OUTPUT_DELTA => {
474                serde_json::from_value(params_value).map(Self::CmdOutputDelta)
475            }
476            methods::FILE_CHANGE_OUTPUT_DELTA => {
477                serde_json::from_value(params_value).map(Self::FileChangeOutputDelta)
478            }
479            methods::REASONING_DELTA => {
480                serde_json::from_value(params_value).map(Self::ReasoningDelta)
481            }
482            methods::ERROR => serde_json::from_value(params_value).map(Self::Error),
483            methods::ACCOUNT_RATE_LIMITS_UPDATED => {
484                serde_json::from_value(params_value).map(Self::AccountRateLimitsUpdated)
485            }
486            methods::MCP_SERVER_STARTUP_STATUS_UPDATED => {
487                serde_json::from_value(params_value).map(Self::McpServerStartupStatusUpdated)
488            }
489            methods::REMOTE_CONTROL_STATUS_CHANGED => {
490                serde_json::from_value(params_value).map(Self::RemoteControlStatusChanged)
491            }
492            methods::MCP_SERVER_OAUTH_LOGIN_COMPLETED => {
493                serde_json::from_value(params_value).map(Self::McpServerOauthLoginCompleted)
494            }
495            methods::FILE_CHANGE_PATCH_UPDATED => {
496                serde_json::from_value(params_value).map(Self::FileChangePatchUpdated)
497            }
498            methods::PLAN_DELTA => serde_json::from_value(params_value).map(Self::PlanDelta),
499            methods::TURN_PLAN_UPDATED => {
500                serde_json::from_value(params_value).map(Self::TurnPlanUpdated)
501            }
502            methods::TURN_DIFF_UPDATED => {
503                serde_json::from_value(params_value).map(Self::TurnDiffUpdated)
504            }
505            methods::REASONING_SUMMARY_PART_ADDED => {
506                serde_json::from_value(params_value).map(Self::ReasoningSummaryPartAdded)
507            }
508            methods::REASONING_TEXT_DELTA => {
509                serde_json::from_value(params_value).map(Self::ReasoningTextDelta)
510            }
511            methods::ACCOUNT_LOGIN_COMPLETED => {
512                serde_json::from_value(params_value).map(Self::AccountLoginCompleted)
513            }
514            methods::DEPRECATION_NOTICE => {
515                serde_json::from_value(params_value).map(Self::DeprecationNotice)
516            }
517            methods::GUARDIAN_WARNING => {
518                serde_json::from_value(params_value).map(Self::GuardianWarning)
519            }
520            methods::WARNING => serde_json::from_value(params_value).map(Self::Warning),
521            methods::THREAD_ARCHIVED => {
522                serde_json::from_value(params_value).map(Self::ThreadArchived)
523            }
524            methods::THREAD_CLOSED => serde_json::from_value(params_value).map(Self::ThreadClosed),
525            methods::THREAD_DELETED => {
526                serde_json::from_value(params_value).map(Self::ThreadDeleted)
527            }
528            methods::THREAD_UNARCHIVED => {
529                serde_json::from_value(params_value).map(Self::ThreadUnarchived)
530            }
531            methods::THREAD_GOAL_CLEARED => {
532                serde_json::from_value(params_value).map(Self::ThreadGoalCleared)
533            }
534            methods::THREAD_NAME_UPDATED => {
535                serde_json::from_value(params_value).map(Self::ThreadNameUpdated)
536            }
537            methods::SKILLS_CHANGED => {
538                serde_json::from_value(params_value).map(Self::SkillsChanged)
539            }
540            methods::FS_CHANGED => serde_json::from_value(params_value).map(Self::FsChanged),
541            methods::CONFIG_WARNING => {
542                serde_json::from_value(params_value).map(Self::ConfigWarning)
543            }
544            methods::ACCOUNT_UPDATED => {
545                serde_json::from_value(params_value).map(Self::AccountUpdated)
546            }
547            methods::APP_LIST_UPDATED => {
548                serde_json::from_value(params_value).map(Self::AppListUpdated)
549            }
550            methods::COMMAND_EXEC_OUTPUT_DELTA => {
551                serde_json::from_value(params_value).map(Self::CommandExecOutputDelta)
552            }
553            methods::EXTERNAL_AGENT_CONFIG_IMPORT_COMPLETED => {
554                serde_json::from_value(params_value).map(Self::ExternalAgentConfigImportCompleted)
555            }
556            methods::FUZZY_FILE_SEARCH_SESSION_COMPLETED => {
557                serde_json::from_value(params_value).map(Self::FuzzyFileSearchSessionCompleted)
558            }
559            methods::FUZZY_FILE_SEARCH_SESSION_UPDATED => {
560                serde_json::from_value(params_value).map(Self::FuzzyFileSearchSessionUpdated)
561            }
562            methods::HOOK_COMPLETED => {
563                serde_json::from_value(params_value).map(Self::HookCompleted)
564            }
565            methods::HOOK_STARTED => serde_json::from_value(params_value).map(Self::HookStarted),
566            methods::ITEM_AUTO_APPROVAL_REVIEW_COMPLETED => {
567                serde_json::from_value(params_value).map(Self::ItemGuardianApprovalReviewCompleted)
568            }
569            methods::ITEM_AUTO_APPROVAL_REVIEW_STARTED => {
570                serde_json::from_value(params_value).map(Self::ItemGuardianApprovalReviewStarted)
571            }
572            methods::ITEM_COMMAND_EXEC_TERMINAL_INTERACTION => {
573                serde_json::from_value(params_value).map(Self::TerminalInteraction)
574            }
575            methods::ITEM_MCP_TOOL_CALL_PROGRESS => {
576                serde_json::from_value(params_value).map(Self::McpToolCallProgress)
577            }
578            methods::MODEL_REROUTED => {
579                serde_json::from_value(params_value).map(Self::ModelRerouted)
580            }
581            methods::MODEL_VERIFICATION => {
582                serde_json::from_value(params_value).map(Self::ModelVerification)
583            }
584            methods::PROCESS_EXITED => {
585                serde_json::from_value(params_value).map(Self::ProcessExited)
586            }
587            methods::PROCESS_OUTPUT_DELTA => {
588                serde_json::from_value(params_value).map(Self::ProcessOutputDelta)
589            }
590            methods::SERVER_REQUEST_RESOLVED => {
591                serde_json::from_value(params_value).map(Self::ServerRequestResolved)
592            }
593            methods::THREAD_COMPACTED => {
594                serde_json::from_value(params_value).map(Self::ContextCompacted)
595            }
596            methods::THREAD_GOAL_UPDATED => {
597                serde_json::from_value(params_value).map(Self::ThreadGoalUpdated)
598            }
599            methods::THREAD_REALTIME_CLOSED => {
600                serde_json::from_value(params_value).map(Self::ThreadRealtimeClosed)
601            }
602            methods::THREAD_REALTIME_ERROR => {
603                serde_json::from_value(params_value).map(Self::ThreadRealtimeError)
604            }
605            methods::THREAD_REALTIME_ITEM_ADDED => {
606                serde_json::from_value(params_value).map(Self::ThreadRealtimeItemAdded)
607            }
608            methods::THREAD_REALTIME_OUTPUT_AUDIO_DELTA => {
609                serde_json::from_value(params_value).map(Self::ThreadRealtimeOutputAudioDelta)
610            }
611            methods::THREAD_REALTIME_SDP => {
612                serde_json::from_value(params_value).map(Self::ThreadRealtimeSdp)
613            }
614            methods::THREAD_REALTIME_STARTED => {
615                serde_json::from_value(params_value).map(Self::ThreadRealtimeStarted)
616            }
617            methods::THREAD_REALTIME_TRANSCRIPT_DELTA => {
618                serde_json::from_value(params_value).map(Self::ThreadRealtimeTranscriptDelta)
619            }
620            methods::THREAD_REALTIME_TRANSCRIPT_DONE => {
621                serde_json::from_value(params_value).map(Self::ThreadRealtimeTranscriptDone)
622            }
623            methods::WINDOWS_WORLD_WRITABLE_WARNING => {
624                serde_json::from_value(params_value).map(Self::WindowsWorldWritableWarning)
625            }
626            methods::WINDOWS_SANDBOX_SETUP_COMPLETED => {
627                serde_json::from_value(params_value).map(Self::WindowsSandboxSetupCompleted)
628            }
629            methods::THREAD_SETTINGS_UPDATED => {
630                serde_json::from_value(params_value).map(Self::ThreadSettingsUpdated)
631            }
632            methods::TURN_MODERATION_METADATA => {
633                serde_json::from_value(params_value).map(Self::TurnModerationMetadata)
634            }
635            methods::EXTERNAL_AGENT_CONFIG_IMPORT_PROGRESS => {
636                serde_json::from_value(params_value).map(Self::ExternalAgentConfigImportProgress)
637            }
638            methods::MODEL_SAFETY_BUFFERING_UPDATED => {
639                serde_json::from_value(params_value).map(Self::ModelSafetyBufferingUpdated)
640            }
641            methods::THREAD_ENVIRONMENT_CONNECTED => {
642                serde_json::from_value(params_value).map(Self::ThreadEnvironmentConnected)
643            }
644            methods::THREAD_ENVIRONMENT_DISCONNECTED => {
645                serde_json::from_value(params_value).map(Self::ThreadEnvironmentDisconnected)
646            }
647            _ => Ok(Self::Unknown {
648                method: method.to_string(),
649                params,
650            }),
651        }
652    }
653
654    /// Decompose this notification back into a `(method, params)` pair.
655    pub fn into_envelope(self) -> Result<(String, Option<Value>), serde_json::Error> {
656        fn pack<T: Serialize>(
657            method: &str,
658            v: &T,
659        ) -> Result<(String, Option<Value>), serde_json::Error> {
660            Ok((method.to_string(), Some(serde_json::to_value(v)?)))
661        }
662        match &self {
663            Self::ThreadStarted(v) => pack(methods::THREAD_STARTED, v),
664            Self::StrictReviewRequired(v) => pack(methods::STRICT_REVIEW_REQUIRED, v),
665            Self::ProjectChanged(v) => pack(methods::PROJECT_CHANGED, v),
666            Self::ThreadProjectUpdated(v) => pack(methods::THREAD_PROJECT_UPDATED, v),
667            Self::ThreadQueueChanged(v) => pack(methods::THREAD_QUEUE_CHANGED, v),
668            Self::ThreadReverted(v) => pack(methods::THREAD_REVERTED, v),
669            Self::McpServerEventStream(v) => pack(methods::MCP_SERVER_EVENT_STREAM, v),
670            Self::ThreadRealtimeItemStarted(v) => pack(methods::THREAD_REALTIME_ITEM_STARTED, v),
671            Self::ThreadRealtimeItemCompleted(v) => {
672                pack(methods::THREAD_REALTIME_ITEM_COMPLETED, v)
673            }
674            Self::ThreadRealtimeItemTranscriptDelta(v) => {
675                pack(methods::THREAD_REALTIME_ITEM_TRANSCRIPT_DELTA, v)
676            }
677            Self::ModelProviderAuthRecoveryStarted(v) => {
678                pack(methods::MODEL_PROVIDER_AUTH_RECOVERY_STARTED, v)
679            }
680            Self::ModelProviderAuthRecoveryCompleted(v) => {
681                pack(methods::MODEL_PROVIDER_AUTH_RECOVERY_COMPLETED, v)
682            }
683            Self::ThreadStatusChanged(v) => pack(methods::THREAD_STATUS_CHANGED, v),
684            Self::ThreadTokenUsageUpdated(v) => pack(methods::THREAD_TOKEN_USAGE_UPDATED, v),
685            Self::TurnStarted(v) => pack(methods::TURN_STARTED, v),
686            Self::TurnCompleted(v) => pack(methods::TURN_COMPLETED, v),
687            Self::ItemStarted(v) => pack(methods::ITEM_STARTED, v),
688            Self::ItemCompleted(v) => pack(methods::ITEM_COMPLETED, v),
689            Self::AgentMessageDelta(v) => pack(methods::AGENT_MESSAGE_DELTA, v),
690            Self::CmdOutputDelta(v) => pack(methods::CMD_OUTPUT_DELTA, v),
691            Self::FileChangeOutputDelta(v) => pack(methods::FILE_CHANGE_OUTPUT_DELTA, v),
692            Self::ReasoningDelta(v) => pack(methods::REASONING_DELTA, v),
693            Self::Error(v) => pack(methods::ERROR, v),
694            Self::AccountRateLimitsUpdated(v) => pack(methods::ACCOUNT_RATE_LIMITS_UPDATED, v),
695            Self::McpServerStartupStatusUpdated(v) => {
696                pack(methods::MCP_SERVER_STARTUP_STATUS_UPDATED, v)
697            }
698            Self::RemoteControlStatusChanged(v) => pack(methods::REMOTE_CONTROL_STATUS_CHANGED, v),
699            Self::McpServerOauthLoginCompleted(v) => {
700                pack(methods::MCP_SERVER_OAUTH_LOGIN_COMPLETED, v)
701            }
702            Self::FileChangePatchUpdated(v) => pack(methods::FILE_CHANGE_PATCH_UPDATED, v),
703            Self::PlanDelta(v) => pack(methods::PLAN_DELTA, v),
704            Self::TurnPlanUpdated(v) => pack(methods::TURN_PLAN_UPDATED, v),
705            Self::TurnDiffUpdated(v) => pack(methods::TURN_DIFF_UPDATED, v),
706            Self::ReasoningSummaryPartAdded(v) => pack(methods::REASONING_SUMMARY_PART_ADDED, v),
707            Self::ReasoningTextDelta(v) => pack(methods::REASONING_TEXT_DELTA, v),
708            Self::AccountLoginCompleted(v) => pack(methods::ACCOUNT_LOGIN_COMPLETED, v),
709            Self::DeprecationNotice(v) => pack(methods::DEPRECATION_NOTICE, v),
710            Self::GuardianWarning(v) => pack(methods::GUARDIAN_WARNING, v),
711            Self::Warning(v) => pack(methods::WARNING, v),
712            Self::ThreadArchived(v) => pack(methods::THREAD_ARCHIVED, v),
713            Self::ThreadClosed(v) => pack(methods::THREAD_CLOSED, v),
714            Self::ThreadDeleted(v) => pack(methods::THREAD_DELETED, v),
715            Self::ThreadUnarchived(v) => pack(methods::THREAD_UNARCHIVED, v),
716            Self::ThreadGoalCleared(v) => pack(methods::THREAD_GOAL_CLEARED, v),
717            Self::ThreadNameUpdated(v) => pack(methods::THREAD_NAME_UPDATED, v),
718            Self::SkillsChanged(v) => pack(methods::SKILLS_CHANGED, v),
719            Self::FsChanged(v) => pack(methods::FS_CHANGED, v),
720            Self::ConfigWarning(v) => pack(methods::CONFIG_WARNING, v),
721            Self::AccountUpdated(v) => pack(methods::ACCOUNT_UPDATED, v),
722            Self::AppListUpdated(v) => pack(methods::APP_LIST_UPDATED, v),
723            Self::CommandExecOutputDelta(v) => pack(methods::COMMAND_EXEC_OUTPUT_DELTA, v),
724            Self::ExternalAgentConfigImportCompleted(v) => {
725                pack(methods::EXTERNAL_AGENT_CONFIG_IMPORT_COMPLETED, v)
726            }
727            Self::FuzzyFileSearchSessionCompleted(v) => {
728                pack(methods::FUZZY_FILE_SEARCH_SESSION_COMPLETED, v)
729            }
730            Self::FuzzyFileSearchSessionUpdated(v) => {
731                pack(methods::FUZZY_FILE_SEARCH_SESSION_UPDATED, v)
732            }
733            Self::HookCompleted(v) => pack(methods::HOOK_COMPLETED, v),
734            Self::HookStarted(v) => pack(methods::HOOK_STARTED, v),
735            Self::ItemGuardianApprovalReviewCompleted(v) => {
736                pack(methods::ITEM_AUTO_APPROVAL_REVIEW_COMPLETED, v)
737            }
738            Self::ItemGuardianApprovalReviewStarted(v) => {
739                pack(methods::ITEM_AUTO_APPROVAL_REVIEW_STARTED, v)
740            }
741            Self::TerminalInteraction(v) => {
742                pack(methods::ITEM_COMMAND_EXEC_TERMINAL_INTERACTION, v)
743            }
744            Self::McpToolCallProgress(v) => pack(methods::ITEM_MCP_TOOL_CALL_PROGRESS, v),
745            Self::ModelRerouted(v) => pack(methods::MODEL_REROUTED, v),
746            Self::ModelVerification(v) => pack(methods::MODEL_VERIFICATION, v),
747            Self::ProcessExited(v) => pack(methods::PROCESS_EXITED, v),
748            Self::ProcessOutputDelta(v) => pack(methods::PROCESS_OUTPUT_DELTA, v),
749            Self::ServerRequestResolved(v) => pack(methods::SERVER_REQUEST_RESOLVED, v),
750            Self::ContextCompacted(v) => pack(methods::THREAD_COMPACTED, v),
751            Self::ThreadGoalUpdated(v) => pack(methods::THREAD_GOAL_UPDATED, v),
752            Self::ThreadRealtimeClosed(v) => pack(methods::THREAD_REALTIME_CLOSED, v),
753            Self::ThreadRealtimeError(v) => pack(methods::THREAD_REALTIME_ERROR, v),
754            Self::ThreadRealtimeItemAdded(v) => pack(methods::THREAD_REALTIME_ITEM_ADDED, v),
755            Self::ThreadRealtimeOutputAudioDelta(v) => {
756                pack(methods::THREAD_REALTIME_OUTPUT_AUDIO_DELTA, v)
757            }
758            Self::ThreadRealtimeSdp(v) => pack(methods::THREAD_REALTIME_SDP, v),
759            Self::ThreadRealtimeStarted(v) => pack(methods::THREAD_REALTIME_STARTED, v),
760            Self::ThreadRealtimeTranscriptDelta(v) => {
761                pack(methods::THREAD_REALTIME_TRANSCRIPT_DELTA, v)
762            }
763            Self::ThreadRealtimeTranscriptDone(v) => {
764                pack(methods::THREAD_REALTIME_TRANSCRIPT_DONE, v)
765            }
766            Self::WindowsWorldWritableWarning(v) => {
767                pack(methods::WINDOWS_WORLD_WRITABLE_WARNING, v)
768            }
769            Self::WindowsSandboxSetupCompleted(v) => {
770                pack(methods::WINDOWS_SANDBOX_SETUP_COMPLETED, v)
771            }
772            Self::ThreadSettingsUpdated(v) => pack(methods::THREAD_SETTINGS_UPDATED, v),
773            Self::TurnModerationMetadata(v) => pack(methods::TURN_MODERATION_METADATA, v),
774            Self::ExternalAgentConfigImportProgress(v) => {
775                pack(methods::EXTERNAL_AGENT_CONFIG_IMPORT_PROGRESS, v)
776            }
777            Self::ModelSafetyBufferingUpdated(v) => {
778                pack(methods::MODEL_SAFETY_BUFFERING_UPDATED, v)
779            }
780            Self::ThreadEnvironmentConnected(v) => pack(methods::THREAD_ENVIRONMENT_CONNECTED, v),
781            Self::ThreadEnvironmentDisconnected(v) => {
782                pack(methods::THREAD_ENVIRONMENT_DISCONNECTED, v)
783            }
784            Self::Unknown { method, params } => Ok((method.clone(), params.clone())),
785        }
786    }
787}
788
789impl Serialize for Notification {
790    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
791        let (method, params) = self
792            .clone()
793            .into_envelope()
794            .map_err(serde::ser::Error::custom)?;
795        let mut env = serde_json::Map::new();
796        env.insert("method".to_string(), Value::String(method));
797        if let Some(p) = params {
798            env.insert("params".to_string(), p);
799        }
800        Value::Object(env).serialize(serializer)
801    }
802}
803
804impl<'de> Deserialize<'de> for Notification {
805    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
806        let value = Value::deserialize(deserializer)?;
807        let method = value
808            .get("method")
809            .and_then(|v| v.as_str())
810            .ok_or_else(|| serde::de::Error::missing_field("method"))?
811            .to_string();
812        let params = value.get("params").cloned();
813        Self::from_envelope(&method, params).map_err(serde::de::Error::custom)
814    }
815}
816
817/// A server-to-client request that requires a response (approval flow).
818///
819/// The wire envelope carries an `id` for response correlation; that `id` is
820/// held alongside this enum in [`ServerMessage::Request`] rather than embedded
821/// inside the variant, since responding doesn't depend on which approval-type
822/// was requested.
823#[derive(Debug, Clone)]
824pub enum ServerRequest {
825    /// `item/commandExecution/requestApproval`
826    CmdExecApproval(CommandExecutionRequestApprovalParams),
827    /// `item/fileChange/requestApproval`
828    FileChangeApproval(FileChangeRequestApprovalParams),
829    /// `item/tool/requestUserInput`
830    ToolRequestUserInput(crate::protocol::ToolRequestUserInputParams),
831    /// `mcpServer/elicitation/request`
832    McpServerElicitationRequest(crate::protocol::McpServerElicitationRequestParams),
833    /// `item/permissions/requestApproval`
834    PermissionsRequestApproval(crate::protocol::PermissionsRequestApprovalParams),
835    /// `item/tool/call`
836    ItemToolCall(crate::protocol::DynamicToolCallParams),
837    /// `account/chatgptAuthTokens/refresh`
838    ChatgptAuthTokensRefresh(crate::protocol::ChatgptAuthTokensRefreshParams),
839    /// `attestation/generate`
840    AttestationGenerate(crate::protocol::AttestationGenerateParams),
841    /// `applyPatchApproval`
842    ApplyPatchApproval(crate::protocol::ApplyPatchApprovalParams),
843    /// `execCommandApproval`
844    ExecCommandApproval(crate::protocol::ExecCommandApprovalParams),
845    /// A request method this crate version does not yet model.
846    Unknown {
847        method: String,
848        params: Option<Value>,
849    },
850}
851
852impl ServerRequest {
853    /// Return the wire `method` string for this request.
854    pub fn method(&self) -> &str {
855        match self {
856            Self::CmdExecApproval(_) => methods::CMD_EXEC_APPROVAL,
857            Self::FileChangeApproval(_) => methods::FILE_CHANGE_APPROVAL,
858            Self::ToolRequestUserInput(_) => methods::TOOL_REQUEST_USER_INPUT,
859            Self::McpServerElicitationRequest(_) => methods::MCP_SERVER_ELICITATION_REQUEST,
860            Self::PermissionsRequestApproval(_) => methods::PERMISSIONS_REQUEST_APPROVAL,
861            Self::ItemToolCall(_) => methods::ITEM_TOOL_CALL,
862            Self::ChatgptAuthTokensRefresh(_) => methods::CHATGPT_AUTH_TOKENS_REFRESH,
863            Self::AttestationGenerate(_) => methods::ATTESTATION_GENERATE,
864            Self::ApplyPatchApproval(_) => methods::APPLY_PATCH_APPROVAL,
865            Self::ExecCommandApproval(_) => methods::EXEC_COMMAND_APPROVAL,
866            Self::Unknown { method, .. } => method,
867        }
868    }
869
870    /// `true` if this request's method isn't modeled by the crate.
871    pub fn is_unknown(&self) -> bool {
872        matches!(self, Self::Unknown { .. })
873    }
874
875    /// Construct a [`ServerRequest`] from a `method` + `params` envelope.
876    pub fn from_envelope(method: &str, params: Option<Value>) -> Result<Self, serde_json::Error> {
877        let params_value = params.clone().unwrap_or(Value::Null);
878        match method {
879            methods::CMD_EXEC_APPROVAL => {
880                serde_json::from_value(params_value).map(Self::CmdExecApproval)
881            }
882            methods::FILE_CHANGE_APPROVAL => {
883                serde_json::from_value(params_value).map(Self::FileChangeApproval)
884            }
885            methods::TOOL_REQUEST_USER_INPUT => {
886                serde_json::from_value(params_value).map(Self::ToolRequestUserInput)
887            }
888            methods::MCP_SERVER_ELICITATION_REQUEST => {
889                serde_json::from_value(params_value).map(Self::McpServerElicitationRequest)
890            }
891            methods::PERMISSIONS_REQUEST_APPROVAL => {
892                serde_json::from_value(params_value).map(Self::PermissionsRequestApproval)
893            }
894            methods::ITEM_TOOL_CALL => serde_json::from_value(params_value).map(Self::ItemToolCall),
895            methods::CHATGPT_AUTH_TOKENS_REFRESH => {
896                serde_json::from_value(params_value).map(Self::ChatgptAuthTokensRefresh)
897            }
898            methods::ATTESTATION_GENERATE => {
899                serde_json::from_value(params_value).map(Self::AttestationGenerate)
900            }
901            methods::APPLY_PATCH_APPROVAL => {
902                serde_json::from_value(params_value).map(Self::ApplyPatchApproval)
903            }
904            methods::EXEC_COMMAND_APPROVAL => {
905                serde_json::from_value(params_value).map(Self::ExecCommandApproval)
906            }
907            _ => Ok(Self::Unknown {
908                method: method.to_string(),
909                params,
910            }),
911        }
912    }
913}
914
915/// A message coming from the app-server.
916///
917/// Replaces the previous loose `{ method, params }` shape with typed enums.
918/// Match on the outer variant first to distinguish notifications (no response)
919/// from requests (need [`crate::AsyncClient::respond`] /
920/// [`crate::SyncClient::respond`]).
921/// Keep variants unboxed so pattern matches and constructors stay ergonomic;
922/// the size skew comes from the generated notification payloads.
923#[allow(clippy::large_enum_variant)]
924#[derive(Debug, Clone)]
925pub enum ServerMessage {
926    /// A notification — no response required.
927    Notification(Notification),
928    /// A request — call `respond(id, ...)` on the client with the matching id.
929    Request {
930        id: RequestId,
931        request: ServerRequest,
932    },
933}
934
935impl ServerMessage {
936    /// `true` if this message is an unmodeled method (notification or request).
937    pub fn is_unknown(&self) -> bool {
938        match self {
939            Self::Notification(n) => n.is_unknown(),
940            Self::Request { request, .. } => request.is_unknown(),
941        }
942    }
943
944    /// Parse a raw app-server frame (one JSON-RPC line) into a [`ServerMessage`].
945    ///
946    /// This is the same parse path the [`AsyncClient`](crate::AsyncClient) and
947    /// [`SyncClient`](crate::SyncClient) run on incoming lines, exposed for
948    /// replay, recovery, and test fixtures that need to decode a captured frame
949    /// without a live client.
950    ///
951    /// A frame is a [`ServerMessage`] only if it is a server-initiated
952    /// notification or request. A JSON-RPC *response* / *error* (a reply to a
953    /// client request) is not a server message and returns
954    /// [`Error::Protocol`](crate::Error::Protocol). Unknown methods decode to
955    /// the `Unknown` variants rather than erroring; a modeled method whose
956    /// `params` don't fit returns [`Error::Deserialization`](crate::Error::Deserialization)
957    /// carrying the raw frame.
958    pub fn from_json_str(s: &str) -> Result<Self, Error> {
959        let msg: JsonRpcMessage = serde_json::from_str(s)
960            .map_err(|e| Error::Deserialization(ParseError::from_line(s, e)))?;
961        Self::from_jsonrpc(msg)
962    }
963
964    /// Parse a [`serde_json::Value`] frame into a [`ServerMessage`].
965    ///
966    /// See [`from_json_str`](Self::from_json_str) for the parsing contract.
967    pub fn from_value(value: Value) -> Result<Self, Error> {
968        let msg: JsonRpcMessage = serde_json::from_value(value.clone())
969            .map_err(|e| Error::Deserialization(ParseError::from_line(value.to_string(), e)))?;
970        Self::from_jsonrpc(msg)
971    }
972
973    fn from_jsonrpc(msg: JsonRpcMessage) -> Result<Self, Error> {
974        match msg {
975            JsonRpcMessage::Notification(JsonRpcNotification { method, params }) => {
976                Notification::from_envelope(&method, params.clone())
977                    .map(ServerMessage::Notification)
978                    .map_err(|e| Error::Deserialization(ParseError::from_envelope(method, params, e)))
979            }
980            JsonRpcMessage::Request(JsonRpcRequest { id, method, params }) => {
981                ServerRequest::from_envelope(&method, params.clone())
982                    .map(|request| ServerMessage::Request { id, request })
983                    .map_err(|e| Error::Deserialization(ParseError::from_envelope(method, params, e)))
984            }
985            JsonRpcMessage::Response(resp) => Err(Error::Protocol(format!(
986                "frame is a JSON-RPC response (id={}), not a server-initiated message",
987                resp.id
988            ))),
989            JsonRpcMessage::Error(err) => Err(Error::Protocol(format!(
990                "frame is a JSON-RPC error response (id={}, code={}), not a server-initiated message",
991                err.id, err.error.code
992            ))),
993        }
994    }
995}
996
997#[cfg(test)]
998mod tests {
999    use super::*;
1000
1001    #[test]
1002    fn test_notification_unknown_method_routes_to_unknown_variant() {
1003        let n = Notification::from_envelope("foo/bar", Some(serde_json::json!({"x": 1})))
1004            .expect("unknown methods do not error");
1005        match n {
1006            Notification::Unknown { method, params } => {
1007                assert_eq!(method, "foo/bar");
1008                assert_eq!(params, Some(serde_json::json!({"x": 1})));
1009            }
1010            other => panic!("expected Unknown, got {:?}", other),
1011        }
1012    }
1013
1014    #[test]
1015    fn test_notification_known_method_with_bad_params_errors() {
1016        // thread/started expects a `thread` field — wrong shape should error.
1017        let err = Notification::from_envelope("thread/started", Some(serde_json::json!({})));
1018        assert!(err.is_err());
1019    }
1020
1021    #[test]
1022    fn test_notification_round_trip_envelope() {
1023        let wire = serde_json::json!({
1024            "method": "item/agentMessage/delta",
1025            "params": {"threadId": "t1", "turnId": "u1", "itemId": "i1", "delta": "hi"},
1026        });
1027        let n: Notification = serde_json::from_value(wire.clone()).unwrap();
1028        assert!(matches!(n, Notification::AgentMessageDelta(_)));
1029        let back = serde_json::to_value(&n).unwrap();
1030        assert_eq!(back, wire);
1031    }
1032
1033    #[test]
1034    fn test_turn_id_from_turn_started() {
1035        // turn/started carries the id under `turn.id`.
1036        let wire = serde_json::json!({
1037            "method": "turn/started",
1038            "params": {"threadId": "t1", "turn": {"id": "turn_42", "status": "inProgress"}},
1039        });
1040        let n: Notification = serde_json::from_value(wire).unwrap();
1041        assert!(matches!(n, Notification::TurnStarted(_)));
1042        assert_eq!(n.turn_id(), Some("turn_42"));
1043    }
1044
1045    #[test]
1046    fn test_turn_id_from_delta_field() {
1047        // Streaming notifications carry a flat `turnId`.
1048        let wire = serde_json::json!({
1049            "method": "item/agentMessage/delta",
1050            "params": {"threadId": "t1", "turnId": "turn_7", "itemId": "i1", "delta": "hi"},
1051        });
1052        let n: Notification = serde_json::from_value(wire).unwrap();
1053        assert_eq!(n.turn_id(), Some("turn_7"));
1054    }
1055
1056    #[test]
1057    fn test_turn_id_none_for_non_turn_notification() {
1058        let n = Notification::from_envelope(
1059            "thread/deleted",
1060            Some(serde_json::json!({"threadId": "t1"})),
1061        )
1062        .unwrap();
1063        assert_eq!(n.turn_id(), None);
1064    }
1065
1066    #[test]
1067    fn test_thread_item_accessor() {
1068        let wire = serde_json::json!({
1069            "method": "item/started",
1070            "params": {
1071                "threadId": "t1",
1072                "turnId": "turn_1",
1073                "startedAtMs": 0,
1074                "item": {"type": "agentMessage", "id": "i1", "text": "hi"},
1075            },
1076        });
1077        let n: Notification = serde_json::from_value(wire).unwrap();
1078        assert!(n.thread_item().is_some());
1079        // A non-item notification has no thread item.
1080        let other = Notification::from_envelope(
1081            "thread/deleted",
1082            Some(serde_json::json!({"threadId": "t1"})),
1083        )
1084        .unwrap();
1085        assert!(other.thread_item().is_none());
1086    }
1087
1088    #[test]
1089    fn test_server_message_from_json_str_notification() {
1090        let line = r#"{"method":"turn/started","params":{"threadId":"t1","turn":{"id":"turn_9","status":"inProgress"}}}"#;
1091        let msg = ServerMessage::from_json_str(line).expect("parses a notification frame");
1092        match msg {
1093            ServerMessage::Notification(n) => assert_eq!(n.turn_id(), Some("turn_9")),
1094            other => panic!("expected Notification, got {other:?}"),
1095        }
1096    }
1097
1098    #[test]
1099    fn test_server_message_from_value_request() {
1100        let frame = serde_json::json!({
1101            "id": 7,
1102            "method": "execCommandApproval",
1103            "params": {
1104                "callId": "c1",
1105                "command": ["ls"],
1106                "conversationId": "th_1",
1107                "cwd": "/tmp",
1108                "parsedCmd": [],
1109            },
1110        });
1111        let msg = ServerMessage::from_value(frame).expect("parses a request frame");
1112        match msg {
1113            ServerMessage::Request { id, request } => {
1114                assert_eq!(id, RequestId::Integer(7));
1115                assert!(matches!(request, ServerRequest::ExecCommandApproval(_)));
1116            }
1117            other => panic!("expected Request, got {other:?}"),
1118        }
1119    }
1120
1121    #[test]
1122    fn test_server_message_from_json_str_rejects_response() {
1123        // A response to a client request is not a server-initiated message.
1124        let line = r#"{"id":1,"result":{"threadId":"th_abc"}}"#;
1125        let err = ServerMessage::from_json_str(line).unwrap_err();
1126        assert!(matches!(err, Error::Protocol(_)), "got: {err:?}");
1127    }
1128
1129    #[test]
1130    fn test_server_message_from_json_str_unknown_method_ok() {
1131        let line = r#"{"method":"some/future/notification","params":{"x":1}}"#;
1132        let msg = ServerMessage::from_json_str(line).unwrap();
1133        assert!(msg.is_unknown());
1134    }
1135}