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