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