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    GatewayOAuthChangedNotification, GuardianWarningNotification, HookCompletedNotification,
40    HookStartedNotification, ItemCompletedNotification,
41    ItemGuardianApprovalReviewCompletedNotification, ItemGuardianApprovalReviewStartedNotification,
42    ItemStartedNotification, McpServerEventStreamNotification,
43    McpServerOauthLoginCompletedNotification, McpServerStatusUpdatedNotification,
44    McpToolCallProgressNotification, ModelReroutedNotification,
45    ModelSafetyBufferingUpdatedNotification, ModelVerificationNotification, PlanDeltaNotification,
46    ProcessExitedNotification, ProcessOutputDeltaNotification, ProjectChangedNotification,
47    ReasoningSummaryPartAddedNotification, ReasoningSummaryTextDeltaNotification,
48    ReasoningTextDeltaNotification, RemoteControlStatusChangedNotification,
49    ServerRequestResolvedNotification, SkillsChangedNotification, StrictReviewRequiredNotification,
50    TerminalInteractionNotification, ThreadArchivedNotification,
51    ThreadAttachmentUpdatedNotification, ThreadClosedNotification, ThreadDeletedNotification,
52    ThreadGoalClearedNotification, ThreadGoalUpdatedNotification, ThreadNameUpdatedNotification,
53    ThreadPredictionUpdatedNotification, ThreadProjectUpdatedNotification,
54    ThreadQueueChangedNotification, ThreadReadStateChangedNotification,
55    ThreadRealtimeClosedNotification, ThreadRealtimeErrorNotification,
56    ThreadRealtimeItemAddedNotification, ThreadRealtimeItemCompletedNotification,
57    ThreadRealtimeItemStartedNotification, ThreadRealtimeItemTranscriptDeltaNotification,
58    ThreadRealtimeOutputAudioDeltaNotification, ThreadRealtimeSdpNotification,
59    ThreadRealtimeStartedNotification, ThreadRealtimeTranscriptDeltaNotification,
60    ThreadRealtimeTranscriptDoneNotification, ThreadRevertedNotification,
61    ThreadSectionChangedNotification, ThreadSettingsUpdatedNotification, ThreadStartedNotification,
62    ThreadStatusChangedNotification, ThreadTokenUsageUpdatedNotification,
63    ThreadUnarchivedNotification, TurnCompletedNotification, TurnDiffUpdatedNotification,
64    TurnModerationMetadataNotification, TurnPlanUpdatedNotification, TurnStartedNotification,
65    WarningNotification, WindowsSandboxSetupCompletedNotification,
66    WindowsWorldWritableWarningNotification,
67};
68use serde::{Deserialize, Deserializer, Serialize, Serializer};
69use serde_json::Value;
70
71/// A server-to-client notification.
72///
73/// Each variant maps to a single `method` string on the wire. The `Unknown`
74/// variant captures methods this crate version doesn't model yet, preserving
75/// the raw payload for inspection.
76#[derive(Debug, Clone)]
77#[allow(clippy::large_enum_variant)] // transient wire values, not stored in bulk
78pub enum Notification {
79    /// `thread/started`
80    ThreadStarted(ThreadStartedNotification),
81    /// `thread/status/changed`
82    ThreadStatusChanged(ThreadStatusChangedNotification),
83    /// `thread/readState/changed` — experimental upstream; durable local
84    /// read receipt for a thread changed.
85    ThreadReadStateChanged(ThreadReadStateChangedNotification),
86    /// `thread/section/changed` — a thread moved into, within, or out of a
87    /// section; broadcast to every connected client.
88    ThreadSectionChanged(ThreadSectionChangedNotification),
89    /// `thread/tokenUsage/updated`
90    ThreadTokenUsageUpdated(ThreadTokenUsageUpdatedNotification),
91    /// `turn/started`
92    TurnStarted(TurnStartedNotification),
93    /// `turn/completed`
94    TurnCompleted(TurnCompletedNotification),
95    /// `item/started`
96    ItemStarted(ItemStartedNotification),
97    /// `item/completed`
98    ItemCompleted(ItemCompletedNotification),
99    /// `item/agentMessage/delta`
100    AgentMessageDelta(AgentMessageDeltaNotification),
101    /// `item/commandExecution/outputDelta`
102    CmdOutputDelta(CommandExecutionOutputDeltaNotification),
103    /// `item/fileChange/outputDelta`
104    FileChangeOutputDelta(FileChangeOutputDeltaNotification),
105    /// `item/reasoning/summaryTextDelta`
106    ReasoningDelta(ReasoningSummaryTextDeltaNotification),
107    /// `error`
108    Error(ErrorNotification),
109    /// `account/rateLimits/updated`
110    AccountRateLimitsUpdated(AccountRateLimitsUpdatedNotification),
111    /// `mcpServer/startupStatus/updated`
112    McpServerStartupStatusUpdated(McpServerStatusUpdatedNotification),
113    /// `remoteControl/status/changed`
114    RemoteControlStatusChanged(RemoteControlStatusChangedNotification),
115    /// `mcpServer/oauthLogin/completed`
116    McpServerOauthLoginCompleted(McpServerOauthLoginCompletedNotification),
117    /// `item/fileChange/patchUpdated`
118    FileChangePatchUpdated(FileChangePatchUpdatedNotification),
119    /// `item/plan/delta` (EXPERIMENTAL)
120    PlanDelta(PlanDeltaNotification),
121    /// `turn/plan/updated`
122    TurnPlanUpdated(TurnPlanUpdatedNotification),
123    /// `turn/diff/updated`
124    TurnDiffUpdated(TurnDiffUpdatedNotification),
125    /// `item/reasoning/summaryPartAdded`
126    ReasoningSummaryPartAdded(ReasoningSummaryPartAddedNotification),
127    /// `item/reasoning/textDelta`
128    ReasoningTextDelta(ReasoningTextDeltaNotification),
129    /// `account/login/completed`
130    AccountLoginCompleted(AccountLoginCompletedNotification),
131    /// `deprecationNotice`
132    DeprecationNotice(DeprecationNoticeNotification),
133    /// `guardianWarning`
134    GuardianWarning(GuardianWarningNotification),
135    /// `warning`
136    Warning(WarningNotification),
137    /// `thread/archived`
138    ThreadArchived(ThreadArchivedNotification),
139    /// `thread/closed`
140    ThreadClosed(ThreadClosedNotification),
141    /// `thread/deleted`
142    ThreadDeleted(ThreadDeletedNotification),
143    /// `thread/unarchived`
144    ThreadUnarchived(ThreadUnarchivedNotification),
145    /// `thread/goal/cleared`
146    ThreadGoalCleared(ThreadGoalClearedNotification),
147    /// `thread/name/updated`
148    ThreadNameUpdated(ThreadNameUpdatedNotification),
149    /// `thread/attachment/updated`
150    ThreadAttachmentUpdated(ThreadAttachmentUpdatedNotification),
151    /// `skills/changed`
152    SkillsChanged(SkillsChangedNotification),
153    /// `fs/changed`
154    FsChanged(FsChangedNotification),
155    /// `configWarning`
156    ConfigWarning(ConfigWarningNotification),
157    /// `account/updated`
158    AccountUpdated(AccountUpdatedNotification),
159    /// `account/gatewayOAuth/changed`
160    GatewayOAuthChanged(GatewayOAuthChangedNotification),
161    /// `app/list/updated`
162    AppListUpdated(AppListUpdatedNotification),
163    /// `command/exec/outputDelta`
164    CommandExecOutputDelta(CommandExecOutputDeltaNotification),
165    /// `externalAgentConfig/import/completed`
166    ExternalAgentConfigImportCompleted(ExternalAgentConfigImportCompletedNotification),
167    /// `fuzzyFileSearch/sessionCompleted`
168    FuzzyFileSearchSessionCompleted(FuzzyFileSearchSessionCompletedNotification),
169    /// `fuzzyFileSearch/sessionUpdated`
170    FuzzyFileSearchSessionUpdated(FuzzyFileSearchSessionUpdatedNotification),
171    /// `hook/completed`
172    HookCompleted(HookCompletedNotification),
173    /// `hook/started`
174    HookStarted(HookStartedNotification),
175    /// `item/autoApprovalReview/completed`
176    ItemGuardianApprovalReviewCompleted(ItemGuardianApprovalReviewCompletedNotification),
177    /// `item/autoApprovalReview/started`
178    ItemGuardianApprovalReviewStarted(ItemGuardianApprovalReviewStartedNotification),
179    /// `item/commandExecution/terminalInteraction`
180    TerminalInteraction(TerminalInteractionNotification),
181    /// `item/mcpToolCall/progress`
182    McpToolCallProgress(McpToolCallProgressNotification),
183    /// `model/rerouted`
184    ModelRerouted(ModelReroutedNotification),
185    /// `model/verification`
186    ModelVerification(ModelVerificationNotification),
187    /// `process/exited`
188    ProcessExited(ProcessExitedNotification),
189    /// `process/outputDelta`
190    ProcessOutputDelta(ProcessOutputDeltaNotification),
191    /// `serverRequest/resolved`
192    ServerRequestResolved(ServerRequestResolvedNotification),
193    /// `thread/compacted`
194    ContextCompacted(ContextCompactedNotification),
195    /// `thread/goal/updated`
196    ThreadGoalUpdated(ThreadGoalUpdatedNotification),
197    /// `thread/realtime/closed`
198    ThreadRealtimeClosed(ThreadRealtimeClosedNotification),
199    /// `thread/realtime/error`
200    ThreadRealtimeError(ThreadRealtimeErrorNotification),
201    /// `thread/realtime/itemAdded`
202    ThreadRealtimeItemAdded(ThreadRealtimeItemAddedNotification),
203    /// `thread/realtime/outputAudio/delta`
204    ThreadRealtimeOutputAudioDelta(ThreadRealtimeOutputAudioDeltaNotification),
205    /// `thread/realtime/sdp`
206    ThreadRealtimeSdp(ThreadRealtimeSdpNotification),
207    /// `thread/realtime/started`
208    ThreadRealtimeStarted(ThreadRealtimeStartedNotification),
209    /// `thread/realtime/transcript/delta`
210    ThreadRealtimeTranscriptDelta(ThreadRealtimeTranscriptDeltaNotification),
211    /// `thread/realtime/transcript/done`
212    ThreadRealtimeTranscriptDone(ThreadRealtimeTranscriptDoneNotification),
213    /// `windows/worldWritableWarning`
214    WindowsWorldWritableWarning(WindowsWorldWritableWarningNotification),
215    /// `windowsSandbox/setupCompleted`
216    WindowsSandboxSetupCompleted(WindowsSandboxSetupCompletedNotification),
217    /// `thread/settings/updated`
218    ThreadSettingsUpdated(ThreadSettingsUpdatedNotification),
219    /// `turn/moderationMetadata`
220    TurnModerationMetadata(TurnModerationMetadataNotification),
221    /// `externalAgentConfig/import/progress`
222    ExternalAgentConfigImportProgress(ExternalAgentConfigImportProgressNotification),
223    /// `model/safetyBuffering/updated`
224    ModelSafetyBufferingUpdated(ModelSafetyBufferingUpdatedNotification),
225    /// `thread/environment/connected`
226    ThreadEnvironmentConnected(EnvironmentConnectionNotification),
227    /// `thread/environment/disconnected`
228    ThreadEnvironmentDisconnected(EnvironmentConnectionNotification),
229    /// `autoApprovalReview/strictReviewRequired` (0.147)
230    StrictReviewRequired(StrictReviewRequiredNotification),
231    /// `project/changed` (0.147)
232    ProjectChanged(ProjectChangedNotification),
233    /// `thread/project/updated` (0.147)
234    ThreadProjectUpdated(ThreadProjectUpdatedNotification),
235    /// `thread/queue/changed` (0.147)
236    ThreadQueueChanged(ThreadQueueChangedNotification),
237    /// `thread/reverted` (0.147)
238    ThreadReverted(ThreadRevertedNotification),
239    /// `mcpServer/event/stream/notification` (0.148 upstream)
240    McpServerEventStream(McpServerEventStreamNotification),
241    /// `thread/realtime/item/started` (0.148 upstream, experimental)
242    ThreadRealtimeItemStarted(ThreadRealtimeItemStartedNotification),
243    /// `thread/realtime/item/completed` (0.148 upstream, experimental)
244    ThreadRealtimeItemCompleted(ThreadRealtimeItemCompletedNotification),
245    /// `thread/realtime/item/transcript/delta` (0.148 upstream, experimental)
246    ThreadRealtimeItemTranscriptDelta(ThreadRealtimeItemTranscriptDeltaNotification),
247    /// `modelProvider/authRecoveryStarted` (0.148 upstream)
248    ModelProviderAuthRecoveryStarted(AuthRecoveryNotification),
249    /// `modelProvider/authRecoveryCompleted` (0.148 upstream)
250    ModelProviderAuthRecoveryCompleted(AuthRecoveryNotification),
251    /// `thread/prediction/updated` (0.159 upstream, experimental)
252    ThreadPredictionUpdated(ThreadPredictionUpdatedNotification),
253    /// A method this crate version does not yet model. The raw params are
254    /// preserved for caller inspection. Encountering this typically means
255    /// the installed codex CLI is newer than the bindings.
256    Unknown {
257        method: String,
258        params: Option<Value>,
259    },
260}
261
262impl Notification {
263    /// Return the wire `method` string for this notification.
264    pub fn method(&self) -> &str {
265        match self {
266            Self::ThreadStarted(_) => methods::THREAD_STARTED,
267            Self::StrictReviewRequired(_) => methods::STRICT_REVIEW_REQUIRED,
268            Self::ProjectChanged(_) => methods::PROJECT_CHANGED,
269            Self::ThreadProjectUpdated(_) => methods::THREAD_PROJECT_UPDATED,
270            Self::ThreadQueueChanged(_) => methods::THREAD_QUEUE_CHANGED,
271            Self::ThreadReverted(_) => methods::THREAD_REVERTED,
272            Self::ThreadPredictionUpdated(_) => methods::THREAD_PREDICTION_UPDATED,
273            Self::McpServerEventStream(_) => methods::MCP_SERVER_EVENT_STREAM,
274            Self::ThreadRealtimeItemStarted(_) => methods::THREAD_REALTIME_ITEM_STARTED,
275            Self::ThreadRealtimeItemCompleted(_) => methods::THREAD_REALTIME_ITEM_COMPLETED,
276            Self::ThreadRealtimeItemTranscriptDelta(_) => {
277                methods::THREAD_REALTIME_ITEM_TRANSCRIPT_DELTA
278            }
279            Self::ModelProviderAuthRecoveryStarted(_) => {
280                methods::MODEL_PROVIDER_AUTH_RECOVERY_STARTED
281            }
282            Self::ModelProviderAuthRecoveryCompleted(_) => {
283                methods::MODEL_PROVIDER_AUTH_RECOVERY_COMPLETED
284            }
285            Self::ThreadStatusChanged(_) => methods::THREAD_STATUS_CHANGED,
286            Self::ThreadReadStateChanged(_) => methods::THREAD_READ_STATE_CHANGED,
287            Self::ThreadSectionChanged(_) => methods::THREAD_SECTION_CHANGED,
288            Self::ThreadTokenUsageUpdated(_) => methods::THREAD_TOKEN_USAGE_UPDATED,
289            Self::TurnStarted(_) => methods::TURN_STARTED,
290            Self::TurnCompleted(_) => methods::TURN_COMPLETED,
291            Self::ItemStarted(_) => methods::ITEM_STARTED,
292            Self::ItemCompleted(_) => methods::ITEM_COMPLETED,
293            Self::AgentMessageDelta(_) => methods::AGENT_MESSAGE_DELTA,
294            Self::CmdOutputDelta(_) => methods::CMD_OUTPUT_DELTA,
295            Self::FileChangeOutputDelta(_) => methods::FILE_CHANGE_OUTPUT_DELTA,
296            Self::ReasoningDelta(_) => methods::REASONING_DELTA,
297            Self::Error(_) => methods::ERROR,
298            Self::AccountRateLimitsUpdated(_) => methods::ACCOUNT_RATE_LIMITS_UPDATED,
299            Self::McpServerStartupStatusUpdated(_) => methods::MCP_SERVER_STARTUP_STATUS_UPDATED,
300            Self::RemoteControlStatusChanged(_) => methods::REMOTE_CONTROL_STATUS_CHANGED,
301            Self::McpServerOauthLoginCompleted(_) => methods::MCP_SERVER_OAUTH_LOGIN_COMPLETED,
302            Self::FileChangePatchUpdated(_) => methods::FILE_CHANGE_PATCH_UPDATED,
303            Self::PlanDelta(_) => methods::PLAN_DELTA,
304            Self::TurnPlanUpdated(_) => methods::TURN_PLAN_UPDATED,
305            Self::TurnDiffUpdated(_) => methods::TURN_DIFF_UPDATED,
306            Self::ReasoningSummaryPartAdded(_) => methods::REASONING_SUMMARY_PART_ADDED,
307            Self::ReasoningTextDelta(_) => methods::REASONING_TEXT_DELTA,
308            Self::AccountLoginCompleted(_) => methods::ACCOUNT_LOGIN_COMPLETED,
309            Self::DeprecationNotice(_) => methods::DEPRECATION_NOTICE,
310            Self::GuardianWarning(_) => methods::GUARDIAN_WARNING,
311            Self::Warning(_) => methods::WARNING,
312            Self::ThreadArchived(_) => methods::THREAD_ARCHIVED,
313            Self::ThreadClosed(_) => methods::THREAD_CLOSED,
314            Self::ThreadDeleted(_) => methods::THREAD_DELETED,
315            Self::ThreadUnarchived(_) => methods::THREAD_UNARCHIVED,
316            Self::ThreadGoalCleared(_) => methods::THREAD_GOAL_CLEARED,
317            Self::ThreadNameUpdated(_) => methods::THREAD_NAME_UPDATED,
318            Self::ThreadAttachmentUpdated(_) => methods::THREAD_ATTACHMENT_UPDATED,
319            Self::SkillsChanged(_) => methods::SKILLS_CHANGED,
320            Self::FsChanged(_) => methods::FS_CHANGED,
321            Self::ConfigWarning(_) => methods::CONFIG_WARNING,
322            Self::AccountUpdated(_) => methods::ACCOUNT_UPDATED,
323            Self::GatewayOAuthChanged(_) => methods::ACCOUNT_GATEWAY_OAUTH_CHANGED,
324            Self::AppListUpdated(_) => methods::APP_LIST_UPDATED,
325            Self::CommandExecOutputDelta(_) => methods::COMMAND_EXEC_OUTPUT_DELTA,
326            Self::ExternalAgentConfigImportCompleted(_) => {
327                methods::EXTERNAL_AGENT_CONFIG_IMPORT_COMPLETED
328            }
329            Self::FuzzyFileSearchSessionCompleted(_) => {
330                methods::FUZZY_FILE_SEARCH_SESSION_COMPLETED
331            }
332            Self::FuzzyFileSearchSessionUpdated(_) => methods::FUZZY_FILE_SEARCH_SESSION_UPDATED,
333            Self::HookCompleted(_) => methods::HOOK_COMPLETED,
334            Self::HookStarted(_) => methods::HOOK_STARTED,
335            Self::ItemGuardianApprovalReviewCompleted(_) => {
336                methods::ITEM_AUTO_APPROVAL_REVIEW_COMPLETED
337            }
338            Self::ItemGuardianApprovalReviewStarted(_) => {
339                methods::ITEM_AUTO_APPROVAL_REVIEW_STARTED
340            }
341            Self::TerminalInteraction(_) => methods::ITEM_COMMAND_EXEC_TERMINAL_INTERACTION,
342            Self::McpToolCallProgress(_) => methods::ITEM_MCP_TOOL_CALL_PROGRESS,
343            Self::ModelRerouted(_) => methods::MODEL_REROUTED,
344            Self::ModelVerification(_) => methods::MODEL_VERIFICATION,
345            Self::ProcessExited(_) => methods::PROCESS_EXITED,
346            Self::ProcessOutputDelta(_) => methods::PROCESS_OUTPUT_DELTA,
347            Self::ServerRequestResolved(_) => methods::SERVER_REQUEST_RESOLVED,
348            Self::ContextCompacted(_) => methods::THREAD_COMPACTED,
349            Self::ThreadGoalUpdated(_) => methods::THREAD_GOAL_UPDATED,
350            Self::ThreadRealtimeClosed(_) => methods::THREAD_REALTIME_CLOSED,
351            Self::ThreadRealtimeError(_) => methods::THREAD_REALTIME_ERROR,
352            Self::ThreadRealtimeItemAdded(_) => methods::THREAD_REALTIME_ITEM_ADDED,
353            Self::ThreadRealtimeOutputAudioDelta(_) => methods::THREAD_REALTIME_OUTPUT_AUDIO_DELTA,
354            Self::ThreadRealtimeSdp(_) => methods::THREAD_REALTIME_SDP,
355            Self::ThreadRealtimeStarted(_) => methods::THREAD_REALTIME_STARTED,
356            Self::ThreadRealtimeTranscriptDelta(_) => methods::THREAD_REALTIME_TRANSCRIPT_DELTA,
357            Self::ThreadRealtimeTranscriptDone(_) => methods::THREAD_REALTIME_TRANSCRIPT_DONE,
358            Self::WindowsWorldWritableWarning(_) => methods::WINDOWS_WORLD_WRITABLE_WARNING,
359            Self::WindowsSandboxSetupCompleted(_) => methods::WINDOWS_SANDBOX_SETUP_COMPLETED,
360            Self::ThreadSettingsUpdated(_) => methods::THREAD_SETTINGS_UPDATED,
361            Self::TurnModerationMetadata(_) => methods::TURN_MODERATION_METADATA,
362            Self::ExternalAgentConfigImportProgress(_) => {
363                methods::EXTERNAL_AGENT_CONFIG_IMPORT_PROGRESS
364            }
365            Self::ModelSafetyBufferingUpdated(_) => methods::MODEL_SAFETY_BUFFERING_UPDATED,
366            Self::ThreadEnvironmentConnected(_) => methods::THREAD_ENVIRONMENT_CONNECTED,
367            Self::ThreadEnvironmentDisconnected(_) => methods::THREAD_ENVIRONMENT_DISCONNECTED,
368            Self::Unknown { method, .. } => method,
369        }
370    }
371
372    /// `true` if this notification's method isn't modeled by the crate.
373    pub fn is_unknown(&self) -> bool {
374        matches!(self, Self::Unknown { .. })
375    }
376
377    /// Return the turn id this notification is scoped to, if it carries one.
378    ///
379    /// Reads the typed `turnId` field (or `turn.id` for the turn-lifecycle
380    /// notifications) directly, so callers don't have to round-trip through
381    /// [`into_envelope`](Self::into_envelope) and poke `serde_json::Value`
382    /// fields. Returns `None` for notifications that aren't turn-scoped, and
383    /// treats an empty id the server omitted as absent.
384    ///
385    /// The turn id from `turn/started` is what
386    /// [`turn/interrupt`](crate::AsyncClient::turn_interrupt) needs to cancel
387    /// the active turn.
388    pub fn turn_id(&self) -> Option<&str> {
389        let id = match self {
390            Self::TurnStarted(n) => n.turn.id.as_str(),
391            Self::TurnCompleted(n) => n.turn.id.as_str(),
392            Self::AgentMessageDelta(n) => n.turn_id.as_str(),
393            Self::CmdOutputDelta(n) => n.turn_id.as_str(),
394            Self::FileChangeOutputDelta(n) => n.turn_id.as_str(),
395            Self::ReasoningDelta(n) => n.turn_id.as_str(),
396            Self::Error(n) => n.turn_id.as_str(),
397            Self::FileChangePatchUpdated(n) => n.turn_id.as_str(),
398            Self::PlanDelta(n) => n.turn_id.as_str(),
399            Self::TurnPlanUpdated(n) => n.turn_id.as_str(),
400            Self::TurnDiffUpdated(n) => n.turn_id.as_str(),
401            Self::TurnModerationMetadata(n) => n.turn_id.as_str(),
402            Self::ReasoningSummaryPartAdded(n) => n.turn_id.as_str(),
403            Self::ReasoningTextDelta(n) => n.turn_id.as_str(),
404            Self::ItemStarted(n) => n.turn_id.as_str(),
405            Self::ItemCompleted(n) => n.turn_id.as_str(),
406            Self::ContextCompacted(n) => n.turn_id.as_str(),
407            Self::McpToolCallProgress(n) => n.turn_id.as_str(),
408            Self::ModelRerouted(n) => n.turn_id.as_str(),
409            Self::ModelVerification(n) => n.turn_id.as_str(),
410            Self::ModelSafetyBufferingUpdated(n) => n.turn_id.as_str(),
411            Self::TerminalInteraction(n) => n.turn_id.as_str(),
412            Self::ThreadTokenUsageUpdated(n) => n.turn_id.as_str(),
413            Self::ItemGuardianApprovalReviewStarted(n) => n.turn_id.as_str(),
414            Self::ItemGuardianApprovalReviewCompleted(n) => n.turn_id.as_str(),
415            _ => return None,
416        };
417        (!id.is_empty()).then_some(id)
418    }
419
420    /// Return the typed [`ThreadItem`](crate::protocol::ThreadItem) this
421    /// notification carries, for `item/started` and `item/completed`.
422    ///
423    /// Lets downstream event adapters read the item's typed fields instead of
424    /// reserializing it back into `serde_json::Value`.
425    pub fn thread_item(&self) -> Option<&crate::protocol::ThreadItem> {
426        match self {
427            Self::ItemStarted(n) => Some(&n.item),
428            Self::ItemCompleted(n) => Some(&n.item),
429            _ => None,
430        }
431    }
432
433    /// Construct a [`Notification`] from a `method` + `params` envelope.
434    ///
435    /// Returns an error if `method` is recognized but `params` doesn't
436    /// deserialize into the typed struct. Unknown methods route to
437    /// [`Notification::Unknown`] without error.
438    pub fn from_envelope(method: &str, params: Option<Value>) -> Result<Self, serde_json::Error> {
439        let params_value = params.clone().unwrap_or(Value::Null);
440        match method {
441            methods::THREAD_STARTED => {
442                serde_json::from_value(params_value).map(Self::ThreadStarted)
443            }
444            methods::STRICT_REVIEW_REQUIRED => {
445                serde_json::from_value(params_value).map(Self::StrictReviewRequired)
446            }
447            methods::PROJECT_CHANGED => {
448                serde_json::from_value(params_value).map(Self::ProjectChanged)
449            }
450            methods::THREAD_PROJECT_UPDATED => {
451                serde_json::from_value(params_value).map(Self::ThreadProjectUpdated)
452            }
453            methods::THREAD_QUEUE_CHANGED => {
454                serde_json::from_value(params_value).map(Self::ThreadQueueChanged)
455            }
456            methods::THREAD_REVERTED => {
457                serde_json::from_value(params_value).map(Self::ThreadReverted)
458            }
459            methods::THREAD_PREDICTION_UPDATED => {
460                serde_json::from_value(params_value).map(Self::ThreadPredictionUpdated)
461            }
462            methods::MCP_SERVER_EVENT_STREAM => {
463                serde_json::from_value(params_value).map(Self::McpServerEventStream)
464            }
465            methods::THREAD_REALTIME_ITEM_STARTED => {
466                serde_json::from_value(params_value).map(Self::ThreadRealtimeItemStarted)
467            }
468            methods::THREAD_REALTIME_ITEM_COMPLETED => {
469                serde_json::from_value(params_value).map(Self::ThreadRealtimeItemCompleted)
470            }
471            methods::THREAD_REALTIME_ITEM_TRANSCRIPT_DELTA => {
472                serde_json::from_value(params_value).map(Self::ThreadRealtimeItemTranscriptDelta)
473            }
474            methods::MODEL_PROVIDER_AUTH_RECOVERY_STARTED => {
475                serde_json::from_value(params_value).map(Self::ModelProviderAuthRecoveryStarted)
476            }
477            methods::MODEL_PROVIDER_AUTH_RECOVERY_COMPLETED => {
478                serde_json::from_value(params_value).map(Self::ModelProviderAuthRecoveryCompleted)
479            }
480            methods::THREAD_STATUS_CHANGED => {
481                serde_json::from_value(params_value).map(Self::ThreadStatusChanged)
482            }
483            methods::THREAD_READ_STATE_CHANGED => {
484                serde_json::from_value(params_value).map(Self::ThreadReadStateChanged)
485            }
486            methods::THREAD_SECTION_CHANGED => {
487                serde_json::from_value(params_value).map(Self::ThreadSectionChanged)
488            }
489            methods::THREAD_TOKEN_USAGE_UPDATED => {
490                serde_json::from_value(params_value).map(Self::ThreadTokenUsageUpdated)
491            }
492            methods::TURN_STARTED => serde_json::from_value(params_value).map(Self::TurnStarted),
493            methods::TURN_COMPLETED => {
494                serde_json::from_value(params_value).map(Self::TurnCompleted)
495            }
496            methods::ITEM_STARTED => serde_json::from_value(params_value).map(Self::ItemStarted),
497            methods::ITEM_COMPLETED => {
498                serde_json::from_value(params_value).map(Self::ItemCompleted)
499            }
500            methods::AGENT_MESSAGE_DELTA => {
501                serde_json::from_value(params_value).map(Self::AgentMessageDelta)
502            }
503            methods::CMD_OUTPUT_DELTA => {
504                serde_json::from_value(params_value).map(Self::CmdOutputDelta)
505            }
506            methods::FILE_CHANGE_OUTPUT_DELTA => {
507                serde_json::from_value(params_value).map(Self::FileChangeOutputDelta)
508            }
509            methods::REASONING_DELTA => {
510                serde_json::from_value(params_value).map(Self::ReasoningDelta)
511            }
512            methods::ERROR => serde_json::from_value(params_value).map(Self::Error),
513            methods::ACCOUNT_RATE_LIMITS_UPDATED => {
514                serde_json::from_value(params_value).map(Self::AccountRateLimitsUpdated)
515            }
516            methods::MCP_SERVER_STARTUP_STATUS_UPDATED => {
517                serde_json::from_value(params_value).map(Self::McpServerStartupStatusUpdated)
518            }
519            methods::REMOTE_CONTROL_STATUS_CHANGED => {
520                serde_json::from_value(params_value).map(Self::RemoteControlStatusChanged)
521            }
522            methods::MCP_SERVER_OAUTH_LOGIN_COMPLETED => {
523                serde_json::from_value(params_value).map(Self::McpServerOauthLoginCompleted)
524            }
525            methods::FILE_CHANGE_PATCH_UPDATED => {
526                serde_json::from_value(params_value).map(Self::FileChangePatchUpdated)
527            }
528            methods::PLAN_DELTA => serde_json::from_value(params_value).map(Self::PlanDelta),
529            methods::TURN_PLAN_UPDATED => {
530                serde_json::from_value(params_value).map(Self::TurnPlanUpdated)
531            }
532            methods::TURN_DIFF_UPDATED => {
533                serde_json::from_value(params_value).map(Self::TurnDiffUpdated)
534            }
535            methods::REASONING_SUMMARY_PART_ADDED => {
536                serde_json::from_value(params_value).map(Self::ReasoningSummaryPartAdded)
537            }
538            methods::REASONING_TEXT_DELTA => {
539                serde_json::from_value(params_value).map(Self::ReasoningTextDelta)
540            }
541            methods::ACCOUNT_LOGIN_COMPLETED => {
542                serde_json::from_value(params_value).map(Self::AccountLoginCompleted)
543            }
544            methods::DEPRECATION_NOTICE => {
545                serde_json::from_value(params_value).map(Self::DeprecationNotice)
546            }
547            methods::GUARDIAN_WARNING => {
548                serde_json::from_value(params_value).map(Self::GuardianWarning)
549            }
550            methods::WARNING => serde_json::from_value(params_value).map(Self::Warning),
551            methods::THREAD_ARCHIVED => {
552                serde_json::from_value(params_value).map(Self::ThreadArchived)
553            }
554            methods::THREAD_CLOSED => serde_json::from_value(params_value).map(Self::ThreadClosed),
555            methods::THREAD_DELETED => {
556                serde_json::from_value(params_value).map(Self::ThreadDeleted)
557            }
558            methods::THREAD_UNARCHIVED => {
559                serde_json::from_value(params_value).map(Self::ThreadUnarchived)
560            }
561            methods::THREAD_GOAL_CLEARED => {
562                serde_json::from_value(params_value).map(Self::ThreadGoalCleared)
563            }
564            methods::THREAD_NAME_UPDATED => {
565                serde_json::from_value(params_value).map(Self::ThreadNameUpdated)
566            }
567            methods::THREAD_ATTACHMENT_UPDATED => {
568                serde_json::from_value(params_value).map(Self::ThreadAttachmentUpdated)
569            }
570            methods::SKILLS_CHANGED => {
571                serde_json::from_value(params_value).map(Self::SkillsChanged)
572            }
573            methods::FS_CHANGED => serde_json::from_value(params_value).map(Self::FsChanged),
574            methods::CONFIG_WARNING => {
575                serde_json::from_value(params_value).map(Self::ConfigWarning)
576            }
577            methods::ACCOUNT_UPDATED => {
578                serde_json::from_value(params_value).map(Self::AccountUpdated)
579            }
580            methods::ACCOUNT_GATEWAY_OAUTH_CHANGED => {
581                serde_json::from_value(params_value).map(Self::GatewayOAuthChanged)
582            }
583            methods::APP_LIST_UPDATED => {
584                serde_json::from_value(params_value).map(Self::AppListUpdated)
585            }
586            methods::COMMAND_EXEC_OUTPUT_DELTA => {
587                serde_json::from_value(params_value).map(Self::CommandExecOutputDelta)
588            }
589            methods::EXTERNAL_AGENT_CONFIG_IMPORT_COMPLETED => {
590                serde_json::from_value(params_value).map(Self::ExternalAgentConfigImportCompleted)
591            }
592            methods::FUZZY_FILE_SEARCH_SESSION_COMPLETED => {
593                serde_json::from_value(params_value).map(Self::FuzzyFileSearchSessionCompleted)
594            }
595            methods::FUZZY_FILE_SEARCH_SESSION_UPDATED => {
596                serde_json::from_value(params_value).map(Self::FuzzyFileSearchSessionUpdated)
597            }
598            methods::HOOK_COMPLETED => {
599                serde_json::from_value(params_value).map(Self::HookCompleted)
600            }
601            methods::HOOK_STARTED => serde_json::from_value(params_value).map(Self::HookStarted),
602            methods::ITEM_AUTO_APPROVAL_REVIEW_COMPLETED => {
603                serde_json::from_value(params_value).map(Self::ItemGuardianApprovalReviewCompleted)
604            }
605            methods::ITEM_AUTO_APPROVAL_REVIEW_STARTED => {
606                serde_json::from_value(params_value).map(Self::ItemGuardianApprovalReviewStarted)
607            }
608            methods::ITEM_COMMAND_EXEC_TERMINAL_INTERACTION => {
609                serde_json::from_value(params_value).map(Self::TerminalInteraction)
610            }
611            methods::ITEM_MCP_TOOL_CALL_PROGRESS => {
612                serde_json::from_value(params_value).map(Self::McpToolCallProgress)
613            }
614            methods::MODEL_REROUTED => {
615                serde_json::from_value(params_value).map(Self::ModelRerouted)
616            }
617            methods::MODEL_VERIFICATION => {
618                serde_json::from_value(params_value).map(Self::ModelVerification)
619            }
620            methods::PROCESS_EXITED => {
621                serde_json::from_value(params_value).map(Self::ProcessExited)
622            }
623            methods::PROCESS_OUTPUT_DELTA => {
624                serde_json::from_value(params_value).map(Self::ProcessOutputDelta)
625            }
626            methods::SERVER_REQUEST_RESOLVED => {
627                serde_json::from_value(params_value).map(Self::ServerRequestResolved)
628            }
629            methods::THREAD_COMPACTED => {
630                serde_json::from_value(params_value).map(Self::ContextCompacted)
631            }
632            methods::THREAD_GOAL_UPDATED => {
633                serde_json::from_value(params_value).map(Self::ThreadGoalUpdated)
634            }
635            methods::THREAD_REALTIME_CLOSED => {
636                serde_json::from_value(params_value).map(Self::ThreadRealtimeClosed)
637            }
638            methods::THREAD_REALTIME_ERROR => {
639                serde_json::from_value(params_value).map(Self::ThreadRealtimeError)
640            }
641            methods::THREAD_REALTIME_ITEM_ADDED => {
642                serde_json::from_value(params_value).map(Self::ThreadRealtimeItemAdded)
643            }
644            methods::THREAD_REALTIME_OUTPUT_AUDIO_DELTA => {
645                serde_json::from_value(params_value).map(Self::ThreadRealtimeOutputAudioDelta)
646            }
647            methods::THREAD_REALTIME_SDP => {
648                serde_json::from_value(params_value).map(Self::ThreadRealtimeSdp)
649            }
650            methods::THREAD_REALTIME_STARTED => {
651                serde_json::from_value(params_value).map(Self::ThreadRealtimeStarted)
652            }
653            methods::THREAD_REALTIME_TRANSCRIPT_DELTA => {
654                serde_json::from_value(params_value).map(Self::ThreadRealtimeTranscriptDelta)
655            }
656            methods::THREAD_REALTIME_TRANSCRIPT_DONE => {
657                serde_json::from_value(params_value).map(Self::ThreadRealtimeTranscriptDone)
658            }
659            methods::WINDOWS_WORLD_WRITABLE_WARNING => {
660                serde_json::from_value(params_value).map(Self::WindowsWorldWritableWarning)
661            }
662            methods::WINDOWS_SANDBOX_SETUP_COMPLETED => {
663                serde_json::from_value(params_value).map(Self::WindowsSandboxSetupCompleted)
664            }
665            methods::THREAD_SETTINGS_UPDATED => {
666                serde_json::from_value(params_value).map(Self::ThreadSettingsUpdated)
667            }
668            methods::TURN_MODERATION_METADATA => {
669                serde_json::from_value(params_value).map(Self::TurnModerationMetadata)
670            }
671            methods::EXTERNAL_AGENT_CONFIG_IMPORT_PROGRESS => {
672                serde_json::from_value(params_value).map(Self::ExternalAgentConfigImportProgress)
673            }
674            methods::MODEL_SAFETY_BUFFERING_UPDATED => {
675                serde_json::from_value(params_value).map(Self::ModelSafetyBufferingUpdated)
676            }
677            methods::THREAD_ENVIRONMENT_CONNECTED => {
678                serde_json::from_value(params_value).map(Self::ThreadEnvironmentConnected)
679            }
680            methods::THREAD_ENVIRONMENT_DISCONNECTED => {
681                serde_json::from_value(params_value).map(Self::ThreadEnvironmentDisconnected)
682            }
683            _ => Ok(Self::Unknown {
684                method: method.to_string(),
685                params,
686            }),
687        }
688    }
689
690    /// Decompose this notification back into a `(method, params)` pair.
691    pub fn into_envelope(self) -> Result<(String, Option<Value>), serde_json::Error> {
692        fn pack<T: Serialize>(
693            method: &str,
694            v: &T,
695        ) -> Result<(String, Option<Value>), serde_json::Error> {
696            Ok((method.to_string(), Some(serde_json::to_value(v)?)))
697        }
698        match &self {
699            Self::ThreadStarted(v) => pack(methods::THREAD_STARTED, v),
700            Self::StrictReviewRequired(v) => pack(methods::STRICT_REVIEW_REQUIRED, v),
701            Self::ProjectChanged(v) => pack(methods::PROJECT_CHANGED, v),
702            Self::ThreadProjectUpdated(v) => pack(methods::THREAD_PROJECT_UPDATED, v),
703            Self::ThreadQueueChanged(v) => pack(methods::THREAD_QUEUE_CHANGED, v),
704            Self::ThreadReverted(v) => pack(methods::THREAD_REVERTED, v),
705            Self::ThreadPredictionUpdated(v) => pack(methods::THREAD_PREDICTION_UPDATED, v),
706            Self::McpServerEventStream(v) => pack(methods::MCP_SERVER_EVENT_STREAM, v),
707            Self::ThreadRealtimeItemStarted(v) => pack(methods::THREAD_REALTIME_ITEM_STARTED, v),
708            Self::ThreadRealtimeItemCompleted(v) => {
709                pack(methods::THREAD_REALTIME_ITEM_COMPLETED, v)
710            }
711            Self::ThreadRealtimeItemTranscriptDelta(v) => {
712                pack(methods::THREAD_REALTIME_ITEM_TRANSCRIPT_DELTA, v)
713            }
714            Self::ModelProviderAuthRecoveryStarted(v) => {
715                pack(methods::MODEL_PROVIDER_AUTH_RECOVERY_STARTED, v)
716            }
717            Self::ModelProviderAuthRecoveryCompleted(v) => {
718                pack(methods::MODEL_PROVIDER_AUTH_RECOVERY_COMPLETED, v)
719            }
720            Self::ThreadStatusChanged(v) => pack(methods::THREAD_STATUS_CHANGED, v),
721            Self::ThreadReadStateChanged(v) => pack(methods::THREAD_READ_STATE_CHANGED, v),
722            Self::ThreadSectionChanged(v) => pack(methods::THREAD_SECTION_CHANGED, v),
723            Self::ThreadTokenUsageUpdated(v) => pack(methods::THREAD_TOKEN_USAGE_UPDATED, v),
724            Self::TurnStarted(v) => pack(methods::TURN_STARTED, v),
725            Self::TurnCompleted(v) => pack(methods::TURN_COMPLETED, v),
726            Self::ItemStarted(v) => pack(methods::ITEM_STARTED, v),
727            Self::ItemCompleted(v) => pack(methods::ITEM_COMPLETED, v),
728            Self::AgentMessageDelta(v) => pack(methods::AGENT_MESSAGE_DELTA, v),
729            Self::CmdOutputDelta(v) => pack(methods::CMD_OUTPUT_DELTA, v),
730            Self::FileChangeOutputDelta(v) => pack(methods::FILE_CHANGE_OUTPUT_DELTA, v),
731            Self::ReasoningDelta(v) => pack(methods::REASONING_DELTA, v),
732            Self::Error(v) => pack(methods::ERROR, v),
733            Self::AccountRateLimitsUpdated(v) => pack(methods::ACCOUNT_RATE_LIMITS_UPDATED, v),
734            Self::McpServerStartupStatusUpdated(v) => {
735                pack(methods::MCP_SERVER_STARTUP_STATUS_UPDATED, v)
736            }
737            Self::RemoteControlStatusChanged(v) => pack(methods::REMOTE_CONTROL_STATUS_CHANGED, v),
738            Self::McpServerOauthLoginCompleted(v) => {
739                pack(methods::MCP_SERVER_OAUTH_LOGIN_COMPLETED, v)
740            }
741            Self::FileChangePatchUpdated(v) => pack(methods::FILE_CHANGE_PATCH_UPDATED, v),
742            Self::PlanDelta(v) => pack(methods::PLAN_DELTA, v),
743            Self::TurnPlanUpdated(v) => pack(methods::TURN_PLAN_UPDATED, v),
744            Self::TurnDiffUpdated(v) => pack(methods::TURN_DIFF_UPDATED, v),
745            Self::ReasoningSummaryPartAdded(v) => pack(methods::REASONING_SUMMARY_PART_ADDED, v),
746            Self::ReasoningTextDelta(v) => pack(methods::REASONING_TEXT_DELTA, v),
747            Self::AccountLoginCompleted(v) => pack(methods::ACCOUNT_LOGIN_COMPLETED, v),
748            Self::DeprecationNotice(v) => pack(methods::DEPRECATION_NOTICE, v),
749            Self::GuardianWarning(v) => pack(methods::GUARDIAN_WARNING, v),
750            Self::Warning(v) => pack(methods::WARNING, v),
751            Self::ThreadArchived(v) => pack(methods::THREAD_ARCHIVED, v),
752            Self::ThreadClosed(v) => pack(methods::THREAD_CLOSED, v),
753            Self::ThreadDeleted(v) => pack(methods::THREAD_DELETED, v),
754            Self::ThreadUnarchived(v) => pack(methods::THREAD_UNARCHIVED, v),
755            Self::ThreadGoalCleared(v) => pack(methods::THREAD_GOAL_CLEARED, v),
756            Self::ThreadNameUpdated(v) => pack(methods::THREAD_NAME_UPDATED, v),
757            Self::ThreadAttachmentUpdated(v) => pack(methods::THREAD_ATTACHMENT_UPDATED, v),
758            Self::SkillsChanged(v) => pack(methods::SKILLS_CHANGED, v),
759            Self::FsChanged(v) => pack(methods::FS_CHANGED, v),
760            Self::ConfigWarning(v) => pack(methods::CONFIG_WARNING, v),
761            Self::AccountUpdated(v) => pack(methods::ACCOUNT_UPDATED, v),
762            Self::GatewayOAuthChanged(v) => pack(methods::ACCOUNT_GATEWAY_OAUTH_CHANGED, v),
763            Self::AppListUpdated(v) => pack(methods::APP_LIST_UPDATED, v),
764            Self::CommandExecOutputDelta(v) => pack(methods::COMMAND_EXEC_OUTPUT_DELTA, v),
765            Self::ExternalAgentConfigImportCompleted(v) => {
766                pack(methods::EXTERNAL_AGENT_CONFIG_IMPORT_COMPLETED, v)
767            }
768            Self::FuzzyFileSearchSessionCompleted(v) => {
769                pack(methods::FUZZY_FILE_SEARCH_SESSION_COMPLETED, v)
770            }
771            Self::FuzzyFileSearchSessionUpdated(v) => {
772                pack(methods::FUZZY_FILE_SEARCH_SESSION_UPDATED, v)
773            }
774            Self::HookCompleted(v) => pack(methods::HOOK_COMPLETED, v),
775            Self::HookStarted(v) => pack(methods::HOOK_STARTED, v),
776            Self::ItemGuardianApprovalReviewCompleted(v) => {
777                pack(methods::ITEM_AUTO_APPROVAL_REVIEW_COMPLETED, v)
778            }
779            Self::ItemGuardianApprovalReviewStarted(v) => {
780                pack(methods::ITEM_AUTO_APPROVAL_REVIEW_STARTED, v)
781            }
782            Self::TerminalInteraction(v) => {
783                pack(methods::ITEM_COMMAND_EXEC_TERMINAL_INTERACTION, v)
784            }
785            Self::McpToolCallProgress(v) => pack(methods::ITEM_MCP_TOOL_CALL_PROGRESS, v),
786            Self::ModelRerouted(v) => pack(methods::MODEL_REROUTED, v),
787            Self::ModelVerification(v) => pack(methods::MODEL_VERIFICATION, v),
788            Self::ProcessExited(v) => pack(methods::PROCESS_EXITED, v),
789            Self::ProcessOutputDelta(v) => pack(methods::PROCESS_OUTPUT_DELTA, v),
790            Self::ServerRequestResolved(v) => pack(methods::SERVER_REQUEST_RESOLVED, v),
791            Self::ContextCompacted(v) => pack(methods::THREAD_COMPACTED, v),
792            Self::ThreadGoalUpdated(v) => pack(methods::THREAD_GOAL_UPDATED, v),
793            Self::ThreadRealtimeClosed(v) => pack(methods::THREAD_REALTIME_CLOSED, v),
794            Self::ThreadRealtimeError(v) => pack(methods::THREAD_REALTIME_ERROR, v),
795            Self::ThreadRealtimeItemAdded(v) => pack(methods::THREAD_REALTIME_ITEM_ADDED, v),
796            Self::ThreadRealtimeOutputAudioDelta(v) => {
797                pack(methods::THREAD_REALTIME_OUTPUT_AUDIO_DELTA, v)
798            }
799            Self::ThreadRealtimeSdp(v) => pack(methods::THREAD_REALTIME_SDP, v),
800            Self::ThreadRealtimeStarted(v) => pack(methods::THREAD_REALTIME_STARTED, v),
801            Self::ThreadRealtimeTranscriptDelta(v) => {
802                pack(methods::THREAD_REALTIME_TRANSCRIPT_DELTA, v)
803            }
804            Self::ThreadRealtimeTranscriptDone(v) => {
805                pack(methods::THREAD_REALTIME_TRANSCRIPT_DONE, v)
806            }
807            Self::WindowsWorldWritableWarning(v) => {
808                pack(methods::WINDOWS_WORLD_WRITABLE_WARNING, v)
809            }
810            Self::WindowsSandboxSetupCompleted(v) => {
811                pack(methods::WINDOWS_SANDBOX_SETUP_COMPLETED, v)
812            }
813            Self::ThreadSettingsUpdated(v) => pack(methods::THREAD_SETTINGS_UPDATED, v),
814            Self::TurnModerationMetadata(v) => pack(methods::TURN_MODERATION_METADATA, v),
815            Self::ExternalAgentConfigImportProgress(v) => {
816                pack(methods::EXTERNAL_AGENT_CONFIG_IMPORT_PROGRESS, v)
817            }
818            Self::ModelSafetyBufferingUpdated(v) => {
819                pack(methods::MODEL_SAFETY_BUFFERING_UPDATED, v)
820            }
821            Self::ThreadEnvironmentConnected(v) => pack(methods::THREAD_ENVIRONMENT_CONNECTED, v),
822            Self::ThreadEnvironmentDisconnected(v) => {
823                pack(methods::THREAD_ENVIRONMENT_DISCONNECTED, v)
824            }
825            Self::Unknown { method, params } => Ok((method.clone(), params.clone())),
826        }
827    }
828}
829
830impl Serialize for Notification {
831    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
832        let (method, params) = self
833            .clone()
834            .into_envelope()
835            .map_err(serde::ser::Error::custom)?;
836        let mut env = serde_json::Map::new();
837        env.insert("method".to_string(), Value::String(method));
838        if let Some(p) = params {
839            env.insert("params".to_string(), p);
840        }
841        Value::Object(env).serialize(serializer)
842    }
843}
844
845impl<'de> Deserialize<'de> for Notification {
846    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
847        let value = Value::deserialize(deserializer)?;
848        let method = value
849            .get("method")
850            .and_then(|v| v.as_str())
851            .ok_or_else(|| serde::de::Error::missing_field("method"))?
852            .to_string();
853        let params = value.get("params").cloned();
854        Self::from_envelope(&method, params).map_err(serde::de::Error::custom)
855    }
856}
857
858/// A server-to-client request that requires a response (approval flow).
859///
860/// The wire envelope carries an `id` for response correlation; that `id` is
861/// held alongside this enum in [`ServerMessage::Request`] rather than embedded
862/// inside the variant, since responding doesn't depend on which approval-type
863/// was requested.
864#[derive(Debug, Clone)]
865pub enum ServerRequest {
866    /// `item/commandExecution/requestApproval`
867    CmdExecApproval(CommandExecutionRequestApprovalParams),
868    /// `item/fileChange/requestApproval`
869    FileChangeApproval(FileChangeRequestApprovalParams),
870    /// `item/tool/requestUserInput`
871    ToolRequestUserInput(crate::protocol::ToolRequestUserInputParams),
872    /// `mcpServer/elicitation/request`
873    McpServerElicitationRequest(crate::protocol::McpServerElicitationRequestParams),
874    /// `item/permissions/requestApproval`
875    PermissionsRequestApproval(crate::protocol::PermissionsRequestApprovalParams),
876    /// `item/tool/call`
877    ItemToolCall(crate::protocol::DynamicToolCallParams),
878    /// `account/chatgptAuthTokens/refresh`
879    ChatgptAuthTokensRefresh(crate::protocol::ChatgptAuthTokensRefreshParams),
880    /// `attestation/generate`
881    AttestationGenerate(crate::protocol::AttestationGenerateParams),
882    /// `applyPatchApproval`
883    ApplyPatchApproval(crate::protocol::ApplyPatchApprovalParams),
884    /// `execCommandApproval`
885    ExecCommandApproval(crate::protocol::ExecCommandApprovalParams),
886    /// A request method this crate version does not yet model.
887    Unknown {
888        method: String,
889        params: Option<Value>,
890    },
891}
892
893impl ServerRequest {
894    /// Return the wire `method` string for this request.
895    pub fn method(&self) -> &str {
896        match self {
897            Self::CmdExecApproval(_) => methods::CMD_EXEC_APPROVAL,
898            Self::FileChangeApproval(_) => methods::FILE_CHANGE_APPROVAL,
899            Self::ToolRequestUserInput(_) => methods::TOOL_REQUEST_USER_INPUT,
900            Self::McpServerElicitationRequest(_) => methods::MCP_SERVER_ELICITATION_REQUEST,
901            Self::PermissionsRequestApproval(_) => methods::PERMISSIONS_REQUEST_APPROVAL,
902            Self::ItemToolCall(_) => methods::ITEM_TOOL_CALL,
903            Self::ChatgptAuthTokensRefresh(_) => methods::CHATGPT_AUTH_TOKENS_REFRESH,
904            Self::AttestationGenerate(_) => methods::ATTESTATION_GENERATE,
905            Self::ApplyPatchApproval(_) => methods::APPLY_PATCH_APPROVAL,
906            Self::ExecCommandApproval(_) => methods::EXEC_COMMAND_APPROVAL,
907            Self::Unknown { method, .. } => method,
908        }
909    }
910
911    /// `true` if this request's method isn't modeled by the crate.
912    pub fn is_unknown(&self) -> bool {
913        matches!(self, Self::Unknown { .. })
914    }
915
916    /// Construct a [`ServerRequest`] from a `method` + `params` envelope.
917    pub fn from_envelope(method: &str, params: Option<Value>) -> Result<Self, serde_json::Error> {
918        let params_value = params.clone().unwrap_or(Value::Null);
919        match method {
920            methods::CMD_EXEC_APPROVAL => {
921                serde_json::from_value(params_value).map(Self::CmdExecApproval)
922            }
923            methods::FILE_CHANGE_APPROVAL => {
924                serde_json::from_value(params_value).map(Self::FileChangeApproval)
925            }
926            methods::TOOL_REQUEST_USER_INPUT => {
927                serde_json::from_value(params_value).map(Self::ToolRequestUserInput)
928            }
929            methods::MCP_SERVER_ELICITATION_REQUEST => {
930                serde_json::from_value(params_value).map(Self::McpServerElicitationRequest)
931            }
932            methods::PERMISSIONS_REQUEST_APPROVAL => {
933                serde_json::from_value(params_value).map(Self::PermissionsRequestApproval)
934            }
935            methods::ITEM_TOOL_CALL => serde_json::from_value(params_value).map(Self::ItemToolCall),
936            methods::CHATGPT_AUTH_TOKENS_REFRESH => {
937                serde_json::from_value(params_value).map(Self::ChatgptAuthTokensRefresh)
938            }
939            methods::ATTESTATION_GENERATE => {
940                serde_json::from_value(params_value).map(Self::AttestationGenerate)
941            }
942            methods::APPLY_PATCH_APPROVAL => {
943                serde_json::from_value(params_value).map(Self::ApplyPatchApproval)
944            }
945            methods::EXEC_COMMAND_APPROVAL => {
946                serde_json::from_value(params_value).map(Self::ExecCommandApproval)
947            }
948            _ => Ok(Self::Unknown {
949                method: method.to_string(),
950                params,
951            }),
952        }
953    }
954}
955
956/// A message coming from the app-server.
957///
958/// Replaces the previous loose `{ method, params }` shape with typed enums.
959/// Match on the outer variant first to distinguish notifications (no response)
960/// from requests (need [`crate::AsyncClient::respond`] /
961/// [`crate::SyncClient::respond`]).
962/// Keep variants unboxed so pattern matches and constructors stay ergonomic;
963/// the size skew comes from the generated notification payloads.
964#[allow(clippy::large_enum_variant)]
965#[derive(Debug, Clone)]
966pub enum ServerMessage {
967    /// A notification — no response required.
968    Notification(Notification),
969    /// A request — call `respond(id, ...)` on the client with the matching id.
970    Request {
971        id: RequestId,
972        request: ServerRequest,
973    },
974}
975
976impl ServerMessage {
977    /// `true` if this message is an unmodeled method (notification or request).
978    pub fn is_unknown(&self) -> bool {
979        match self {
980            Self::Notification(n) => n.is_unknown(),
981            Self::Request { request, .. } => request.is_unknown(),
982        }
983    }
984
985    /// Parse a raw app-server frame (one JSON-RPC line) into a [`ServerMessage`].
986    ///
987    /// This is the same parse path the [`AsyncClient`](crate::AsyncClient) and
988    /// [`SyncClient`](crate::SyncClient) run on incoming lines, exposed for
989    /// replay, recovery, and test fixtures that need to decode a captured frame
990    /// without a live client.
991    ///
992    /// A frame is a [`ServerMessage`] only if it is a server-initiated
993    /// notification or request. A JSON-RPC *response* / *error* (a reply to a
994    /// client request) is not a server message and returns
995    /// [`Error::Protocol`](crate::Error::Protocol). Unknown methods decode to
996    /// the `Unknown` variants rather than erroring; a modeled method whose
997    /// `params` don't fit returns [`Error::Deserialization`](crate::Error::Deserialization)
998    /// carrying the raw frame.
999    pub fn from_json_str(s: &str) -> Result<Self, Error> {
1000        let msg: JsonRpcMessage = serde_json::from_str(s)
1001            .map_err(|e| Error::Deserialization(ParseError::from_line(s, e)))?;
1002        Self::from_jsonrpc(msg)
1003    }
1004
1005    /// Parse a [`serde_json::Value`] frame into a [`ServerMessage`].
1006    ///
1007    /// See [`from_json_str`](Self::from_json_str) for the parsing contract.
1008    pub fn from_value(value: Value) -> Result<Self, Error> {
1009        let msg: JsonRpcMessage = serde_json::from_value(value.clone())
1010            .map_err(|e| Error::Deserialization(ParseError::from_line(value.to_string(), e)))?;
1011        Self::from_jsonrpc(msg)
1012    }
1013
1014    fn from_jsonrpc(msg: JsonRpcMessage) -> Result<Self, Error> {
1015        match msg {
1016            JsonRpcMessage::Notification(JsonRpcNotification { method, params }) => {
1017                Notification::from_envelope(&method, params.clone())
1018                    .map(ServerMessage::Notification)
1019                    .map_err(|e| Error::Deserialization(ParseError::from_envelope(method, params, e)))
1020            }
1021            JsonRpcMessage::Request(JsonRpcRequest { id, method, params }) => {
1022                ServerRequest::from_envelope(&method, params.clone())
1023                    .map(|request| ServerMessage::Request { id, request })
1024                    .map_err(|e| Error::Deserialization(ParseError::from_envelope(method, params, e)))
1025            }
1026            JsonRpcMessage::Response(resp) => Err(Error::Protocol(format!(
1027                "frame is a JSON-RPC response (id={}), not a server-initiated message",
1028                resp.id
1029            ))),
1030            JsonRpcMessage::Error(err) => Err(Error::Protocol(format!(
1031                "frame is a JSON-RPC error response (id={}, code={}), not a server-initiated message",
1032                err.id, err.error.code
1033            ))),
1034        }
1035    }
1036}
1037
1038#[cfg(test)]
1039mod tests {
1040    use super::*;
1041
1042    #[test]
1043    fn test_notification_unknown_method_routes_to_unknown_variant() {
1044        let n = Notification::from_envelope("foo/bar", Some(serde_json::json!({"x": 1})))
1045            .expect("unknown methods do not error");
1046        match n {
1047            Notification::Unknown { method, params } => {
1048                assert_eq!(method, "foo/bar");
1049                assert_eq!(params, Some(serde_json::json!({"x": 1})));
1050            }
1051            other => panic!("expected Unknown, got {:?}", other),
1052        }
1053    }
1054
1055    #[test]
1056    fn test_notification_known_method_with_bad_params_errors() {
1057        // thread/started expects a `thread` field — wrong shape should error.
1058        let err = Notification::from_envelope("thread/started", Some(serde_json::json!({})));
1059        assert!(err.is_err());
1060    }
1061
1062    #[test]
1063    fn test_notification_round_trip_envelope() {
1064        let wire = serde_json::json!({
1065            "method": "item/agentMessage/delta",
1066            "params": {"threadId": "t1", "turnId": "u1", "itemId": "i1", "delta": "hi"},
1067        });
1068        let n: Notification = serde_json::from_value(wire.clone()).unwrap();
1069        assert!(matches!(n, Notification::AgentMessageDelta(_)));
1070        let back = serde_json::to_value(&n).unwrap();
1071        assert_eq!(back, wire);
1072    }
1073
1074    #[test]
1075    fn test_model_provider_error_round_trips_from_jsonrpc_error_data() {
1076        use crate::protocol_generated::types::ModelProviderError;
1077
1078        let wire = serde_json::json!({
1079            "code": -32600,
1080            "message": "thread was created with a different model provider",
1081            "data": {
1082                "type": "modelProviderMismatch",
1083                "modelProvider": "old-provider",
1084                "currentModelProvider": "current-provider",
1085            },
1086        });
1087        let err: crate::JsonRpcErrorData = serde_json::from_value(wire.clone()).unwrap();
1088        let data: ModelProviderError = serde_json::from_value(err.data.unwrap()).unwrap();
1089        assert_eq!(
1090            data,
1091            ModelProviderError::ModelProviderMismatch {
1092                model_provider: "old-provider".to_string(),
1093                current_model_provider: "current-provider".to_string(),
1094            }
1095        );
1096        assert_eq!(serde_json::to_value(&data).unwrap(), wire["data"]);
1097
1098        for (wire, expected) in [
1099            (
1100                serde_json::json!({
1101                    "type": "modelProviderNotConfigured",
1102                    "modelProvider": "old-provider",
1103                }),
1104                ModelProviderError::ModelProviderNotConfigured {
1105                    model_provider: "old-provider".to_string(),
1106                },
1107            ),
1108            (
1109                serde_json::json!({
1110                    "type": "modelProviderPolicyConflict",
1111                    "modelProvider": "old-provider",
1112                    "requiredModelProvider": "required-provider",
1113                }),
1114                ModelProviderError::ModelProviderPolicyConflict {
1115                    model_provider: "old-provider".to_string(),
1116                    required_model_provider: "required-provider".to_string(),
1117                },
1118            ),
1119        ] {
1120            assert_eq!(
1121                serde_json::from_value::<ModelProviderError>(wire.clone()).unwrap(),
1122                expected
1123            );
1124            assert_eq!(serde_json::to_value(&expected).unwrap(), wire);
1125        }
1126    }
1127
1128    #[test]
1129    fn test_model_provider_list_response_round_trips() {
1130        use crate::protocol_generated::types::{
1131            GatewayAuth, ModelProviderListResponse, ProviderSettings,
1132        };
1133
1134        let wire = serde_json::json!({
1135            "data": [
1136                {"id": "openai", "name": "OpenAI", "canEdit": false,
1137                 "baseUrl": null, "settings": {"type": "openai"}},
1138                {"id": "gw", "name": "Gateway", "canEdit": true,
1139                 "baseUrl": "https://gw.example",
1140                 "settings": {"type": "customGateway",
1141                              "auth": {"mode": "environmentVariable", "envKey": "GW_KEY"}}},
1142                {"id": "ext", "name": "External", "canEdit": true, "baseUrl": null,
1143                 "settings": {"type": "customGateway",
1144                              "auth": {"mode": "externallyConfigured", "envKey": null}}},
1145                {"id": "br", "name": "Bedrock", "canEdit": true, "baseUrl": null,
1146                 "settings": {"type": "bedrock", "profile": "dev", "region": null}},
1147                {"id": "vx", "name": "Vertex", "canEdit": false, "baseUrl": null,
1148                 "settings": {"type": "gcp", "projectId": "p", "location": null,
1149                              "credentialFile": "/creds.json"}},
1150            ],
1151            "defaultProviderId": "openai",
1152            "canChangeDefault": true,
1153        });
1154        let resp: ModelProviderListResponse = serde_json::from_value(wire.clone()).unwrap();
1155        assert_eq!(resp.default_provider_id, "openai");
1156        assert!(resp.can_change_default);
1157        assert_eq!(resp.data[0].settings, ProviderSettings::OpenAi);
1158        assert_eq!(
1159            resp.data[1].settings,
1160            ProviderSettings::CustomGateway {
1161                auth: GatewayAuth::EnvironmentVariable {
1162                    env_key: "GW_KEY".to_string()
1163                }
1164            }
1165        );
1166        assert_eq!(
1167            resp.data[4].settings,
1168            ProviderSettings::Gcp {
1169                project_id: Some("p".to_string()),
1170                location: None,
1171                credential_file: Some("/creds.json".to_string()),
1172            }
1173        );
1174        assert_eq!(serde_json::to_value(&resp).unwrap(), wire);
1175
1176        let none: GatewayAuth =
1177            serde_json::from_value(serde_json::json!({"mode": "none"})).unwrap();
1178        assert_eq!(none, GatewayAuth::None);
1179    }
1180
1181    #[test]
1182    fn test_model_provider_config_write_patch_round_trips() {
1183        use crate::protocol_generated::types::{
1184            BedrockProviderId, GcpProviderId, ModelProviderConfigWriteParams, ProviderSettingsPatch,
1185        };
1186
1187        // Omitted preserves, explicit null clears, a value sets.
1188        let wire = serde_json::json!({
1189            "settings": {"type": "bedrock", "providerId": "amazon-bedrock-runtime",
1190                         "region": null}
1191        });
1192        let params: ModelProviderConfigWriteParams = serde_json::from_value(wire.clone()).unwrap();
1193        assert_eq!(
1194            params.settings,
1195            ProviderSettingsPatch::Bedrock {
1196                provider_id: BedrockProviderId::AmazonBedrockRuntime,
1197                profile: None,
1198                region: Some(None),
1199            }
1200        );
1201        assert_eq!(serde_json::to_value(&params).unwrap(), wire);
1202
1203        let gcp = ProviderSettingsPatch::Gcp {
1204            provider_id: GcpProviderId::Gcp,
1205            project_id: Some(Some("p".to_string())),
1206            location: None,
1207            credential_file: Some(None),
1208        };
1209        assert_eq!(
1210            serde_json::to_value(&gcp).unwrap(),
1211            serde_json::json!({"type": "gcp", "providerId": "gcp",
1212                               "projectId": "p", "credentialFile": null})
1213        );
1214
1215        let gateway: ProviderSettingsPatch = serde_json::from_value(serde_json::json!({
1216            "type": "customGateway", "name": "GW", "baseUrl": "https://gw.example",
1217            "envKey": null
1218        }))
1219        .unwrap();
1220        assert_eq!(
1221            gateway,
1222            ProviderSettingsPatch::CustomGateway {
1223                provider_id: None,
1224                name: Some("GW".to_string()),
1225                base_url: Some(Some("https://gw.example".to_string())),
1226                env_key: Some(None),
1227            }
1228        );
1229
1230        // The name can be omitted but not cleared.
1231        assert!(serde_json::from_value::<ProviderSettingsPatch>(
1232            serde_json::json!({"type": "customGateway", "name": null})
1233        )
1234        .is_err());
1235    }
1236
1237    #[test]
1238    fn test_model_provider_default_set_round_trips() {
1239        use crate::protocol_generated::types::ModelProviderDefaultSetParams;
1240
1241        let wire = serde_json::json!({"providerId": "amazon-bedrock"});
1242        let params: ModelProviderDefaultSetParams = serde_json::from_value(wire.clone()).unwrap();
1243        assert_eq!(params.provider_id, "amazon-bedrock");
1244        assert_eq!(serde_json::to_value(&params).unwrap(), wire);
1245        assert!(
1246            serde_json::from_value::<ModelProviderDefaultSetParams>(serde_json::json!({})).is_err()
1247        );
1248    }
1249
1250    #[test]
1251    fn test_thread_list_included_thread_ids_round_trips() {
1252        use crate::protocol_generated::types::ThreadListParams;
1253
1254        let params = ThreadListParams {
1255            included_thread_ids: Some(vec!["t1".to_string()]),
1256            ..Default::default()
1257        };
1258        assert_eq!(
1259            serde_json::to_value(&params).unwrap(),
1260            serde_json::json!({"includedThreadIds": ["t1"]})
1261        );
1262        assert_eq!(
1263            serde_json::to_value(ThreadListParams::default()).unwrap(),
1264            serde_json::json!({})
1265        );
1266    }
1267
1268    #[test]
1269    fn test_thread_realtime_persisted_items_round_trip() {
1270        use crate::protocol_generated::types::{
1271            ThreadRealtimeCoverage, ThreadRealtimeEntry, ThreadRealtimeHandoffMode,
1272            ThreadRealtimeItem, ThreadRealtimeItemDetail, ThreadRealtimeOrder,
1273            ThreadRealtimeTranscriptRole,
1274        };
1275
1276        let frame = serde_json::json!({
1277            "id": "f1", "realtimeSessionId": "voice", "observedAtMs": 1_700_000_000_000_i64,
1278            "type": "frame",
1279            "channels": {"voice_transcript": {"role": "user", "text": "hi"}}
1280        });
1281        let item: ThreadRealtimeItem = serde_json::from_value(frame.clone()).unwrap();
1282        assert_eq!(item.observed_at_ms, Some(1_700_000_000_000));
1283        match &item.detail {
1284            ThreadRealtimeItemDetail::Frame { channels } => {
1285                assert_eq!(
1286                    channels.voice_transcript.role,
1287                    ThreadRealtimeTranscriptRole::User
1288                );
1289                assert_eq!(channels.voice_transcript.text, "hi");
1290            }
1291            other => panic!("expected frame, got {other:?}"),
1292        }
1293        assert_eq!(serde_json::to_value(&item).unwrap(), frame);
1294
1295        // An undated live item keeps its wire shape (no observedAtMs key).
1296        let live = serde_json::json!({
1297            "id": "speech", "realtimeSessionId": "voice", "type": "transcriptSegment",
1298            "role": "assistant", "text": "hello"
1299        });
1300        let item: ThreadRealtimeItem = serde_json::from_value(live.clone()).unwrap();
1301        assert_eq!(item.observed_at_ms, None);
1302        assert_eq!(serde_json::to_value(&item).unwrap(), live);
1303
1304        let accepted = serde_json::json!({
1305            "id": "a1", "realtimeSessionId": "voice", "type": "turnInputAccepted",
1306            "handoff_id": "h1", "turn_id": "turn1", "mode": "steer",
1307            "source_event_ids": ["e1", "e2"], "input_item_id": "in1", "input": "do it"
1308        });
1309        let entry: ThreadRealtimeEntry = serde_json::from_value(serde_json::json!({
1310            "position": 7, "item": accepted.clone()
1311        }))
1312        .unwrap();
1313        assert_eq!(entry.position, 7);
1314        match &entry.item.detail {
1315            ThreadRealtimeItemDetail::TurnInputAccepted {
1316                mode,
1317                source_event_ids,
1318                ..
1319            } => {
1320                assert_eq!(*mode, ThreadRealtimeHandoffMode::Steer);
1321                assert_eq!(source_event_ids.len(), 2);
1322            }
1323            other => panic!("expected turnInputAccepted, got {other:?}"),
1324        }
1325        assert_eq!(serde_json::to_value(&entry.item).unwrap(), accepted);
1326
1327        let saved = serde_json::json!({
1328            "id": "s1", "realtimeSessionId": "voice", "type": "sessionEndInputSaved",
1329            "source_event_ids": [], "input_item_id": "in2", "input": "later"
1330        });
1331        let item: ThreadRealtimeItem = serde_json::from_value(saved.clone()).unwrap();
1332        assert!(matches!(
1333            item.detail,
1334            ThreadRealtimeItemDetail::SessionEndInputSaved { .. }
1335        ));
1336        assert_eq!(serde_json::to_value(&item).unwrap(), saved);
1337
1338        assert_eq!(
1339            ThreadRealtimeOrder::default(),
1340            ThreadRealtimeOrder::ObservedAt
1341        );
1342        assert_eq!(
1343            serde_json::to_value(ThreadRealtimeOrder::ObservedAt).unwrap(),
1344            "observedAt"
1345        );
1346        assert_eq!(
1347            serde_json::from_value::<ThreadRealtimeCoverage>(serde_json::json!("pending")).unwrap(),
1348            ThreadRealtimeCoverage::Pending
1349        );
1350    }
1351
1352    #[test]
1353    fn test_human_command_approval_request_decodes() {
1354        use crate::protocol_generated::types::CommandExecutionApprovalKind;
1355
1356        let msg = ServerMessage::from_value(serde_json::json!({
1357            "jsonrpc": "2.0",
1358            "id": 7,
1359            "method": "item/commandExecution/requestApproval",
1360            "params": {
1361                "threadId": "t1",
1362                "turnId": "u1",
1363                "itemId": "i1",
1364                "startedAtMs": 0,
1365                "kind": "humanCommand",
1366            },
1367        }))
1368        .unwrap();
1369        match msg {
1370            ServerMessage::Request {
1371                request: ServerRequest::CmdExecApproval(params),
1372                ..
1373            } => assert_eq!(
1374                params.kind,
1375                Some(CommandExecutionApprovalKind::HumanCommand)
1376            ),
1377            other => panic!("expected CmdExecApproval, got {:?}", other),
1378        }
1379    }
1380
1381    #[test]
1382    fn test_codex_error_info_other_accepts_unknown_codes() {
1383        use crate::protocol_generated::types::CodexErrorInfo;
1384
1385        // Known codes still decode to their typed variants, tagged or not.
1386        assert_eq!(
1387            serde_json::from_value::<CodexErrorInfo>(serde_json::json!("contextWindowExceeded"))
1388                .unwrap(),
1389            CodexErrorInfo::ContextWindowExceeded
1390        );
1391        assert_eq!(
1392            serde_json::from_value::<CodexErrorInfo>(serde_json::json!("modelNotFound")).unwrap(),
1393            CodexErrorInfo::ModelNotFound
1394        );
1395        assert_eq!(
1396            serde_json::from_value::<CodexErrorInfo>(serde_json::json!(
1397                "policyCapacityUnavailable"
1398            ))
1399            .unwrap(),
1400            CodexErrorInfo::PolicyCapacityUnavailable
1401        );
1402        assert_eq!(
1403            serde_json::from_value::<CodexErrorInfo>(serde_json::json!("inferenceHookDenied"))
1404                .unwrap(),
1405            CodexErrorInfo::InferenceHookDenied
1406        );
1407        assert_eq!(
1408            serde_json::from_value::<CodexErrorInfo>(
1409                serde_json::json!({"httpConnectionFailed": {"httpStatusCode": 502}})
1410            )
1411            .unwrap(),
1412            CodexErrorInfo::HttpConnectionFailed {
1413                http_status_code: Some(502)
1414            }
1415        );
1416
1417        // Unknown strings and objects fall through to `Other` instead of erroring.
1418        for unknown in [
1419            serde_json::json!("other"),
1420            serde_json::json!("bioPolicy"),
1421            serde_json::json!({"someFutureVariant": {"detail": 1}}),
1422        ] {
1423            assert_eq!(
1424                serde_json::from_value::<CodexErrorInfo>(unknown).unwrap(),
1425                CodexErrorInfo::Other
1426            );
1427        }
1428        assert!(serde_json::from_value::<CodexErrorInfo>(serde_json::json!(42)).is_err());
1429        assert!(serde_json::from_value::<CodexErrorInfo>(serde_json::Value::Null).is_err());
1430
1431        // `Other` always serializes back to the canonical string.
1432        assert_eq!(
1433            serde_json::to_value(CodexErrorInfo::Other).unwrap(),
1434            serde_json::json!("other")
1435        );
1436    }
1437
1438    #[test]
1439    fn test_thread_goal_params_round_trip_origin() {
1440        use crate::protocol_generated::types::{
1441            ThreadGoalClearParams, ThreadGoalMutationOrigin, ThreadGoalSetParams,
1442        };
1443
1444        let set = ThreadGoalSetParams {
1445            thread_id: "t1".into(),
1446            objective: Some("ship it".into()),
1447            origin: Some(ThreadGoalMutationOrigin::User),
1448            ..Default::default()
1449        };
1450        assert_eq!(
1451            serde_json::to_value(&set).unwrap(),
1452            serde_json::json!({"threadId": "t1", "objective": "ship it", "origin": "user"})
1453        );
1454
1455        let clear: ThreadGoalClearParams =
1456            serde_json::from_value(serde_json::json!({"threadId": "t1", "origin": "automatic"}))
1457                .unwrap();
1458        assert_eq!(clear.origin, Some(ThreadGoalMutationOrigin::Automatic));
1459
1460        // Older callers omit `origin`; it must stay optional and absent on the wire.
1461        let bare: ThreadGoalClearParams =
1462            serde_json::from_value(serde_json::json!({"threadId": "t1"})).unwrap();
1463        assert_eq!(bare.origin, None);
1464        assert_eq!(
1465            serde_json::to_value(&bare).unwrap(),
1466            serde_json::json!({"threadId": "t1"})
1467        );
1468    }
1469
1470    #[test]
1471    fn test_thread_prediction_updated_round_trips_both_results() {
1472        use crate::protocol_generated::types::ThreadPredictionResult;
1473
1474        for (result, expected) in [
1475            (
1476                serde_json::json!({"type": "completed", "text": "next prompt"}),
1477                ThreadPredictionResult::Completed {
1478                    text: Some("next prompt".into()),
1479                },
1480            ),
1481            (
1482                serde_json::json!({"type": "completed"}),
1483                ThreadPredictionResult::Completed { text: None },
1484            ),
1485            (
1486                serde_json::json!({"type": "failed"}),
1487                ThreadPredictionResult::Failed,
1488            ),
1489        ] {
1490            let wire = serde_json::json!({
1491                "method": "thread/prediction/updated",
1492                "params": {"result": result, "sourceTurnId": "u1", "threadId": "t1"},
1493            });
1494            let n: Notification = serde_json::from_value(wire.clone()).unwrap();
1495            let Notification::ThreadPredictionUpdated(p) = &n else {
1496                panic!("expected ThreadPredictionUpdated, got {:?}", n);
1497            };
1498            assert_eq!(p.result, expected);
1499            assert_eq!(p.source_turn_id, "u1");
1500            assert_eq!(serde_json::to_value(&n).unwrap(), wire);
1501        }
1502    }
1503
1504    #[test]
1505    fn test_turn_id_from_turn_started() {
1506        // turn/started carries the id under `turn.id`.
1507        let wire = serde_json::json!({
1508            "method": "turn/started",
1509            "params": {"threadId": "t1", "turn": {"id": "turn_42", "status": "inProgress"}},
1510        });
1511        let n: Notification = serde_json::from_value(wire).unwrap();
1512        assert!(matches!(n, Notification::TurnStarted(_)));
1513        assert_eq!(n.turn_id(), Some("turn_42"));
1514    }
1515
1516    #[test]
1517    fn test_turn_id_from_delta_field() {
1518        // Streaming notifications carry a flat `turnId`.
1519        let wire = serde_json::json!({
1520            "method": "item/agentMessage/delta",
1521            "params": {"threadId": "t1", "turnId": "turn_7", "itemId": "i1", "delta": "hi"},
1522        });
1523        let n: Notification = serde_json::from_value(wire).unwrap();
1524        assert_eq!(n.turn_id(), Some("turn_7"));
1525    }
1526
1527    #[test]
1528    fn test_turn_id_none_for_non_turn_notification() {
1529        let n = Notification::from_envelope(
1530            "thread/deleted",
1531            Some(serde_json::json!({"threadId": "t1"})),
1532        )
1533        .unwrap();
1534        assert_eq!(n.turn_id(), None);
1535    }
1536
1537    #[test]
1538    fn test_thread_item_accessor() {
1539        let wire = serde_json::json!({
1540            "method": "item/started",
1541            "params": {
1542                "threadId": "t1",
1543                "turnId": "turn_1",
1544                "startedAtMs": 0,
1545                "item": {"type": "agentMessage", "id": "i1", "text": "hi"},
1546            },
1547        });
1548        let n: Notification = serde_json::from_value(wire).unwrap();
1549        assert!(n.thread_item().is_some());
1550        // A non-item notification has no thread item.
1551        let other = Notification::from_envelope(
1552            "thread/deleted",
1553            Some(serde_json::json!({"threadId": "t1"})),
1554        )
1555        .unwrap();
1556        assert!(other.thread_item().is_none());
1557    }
1558
1559    #[test]
1560    fn test_server_message_from_json_str_notification() {
1561        let line = r#"{"method":"turn/started","params":{"threadId":"t1","turn":{"id":"turn_9","status":"inProgress"}}}"#;
1562        let msg = ServerMessage::from_json_str(line).expect("parses a notification frame");
1563        match msg {
1564            ServerMessage::Notification(n) => assert_eq!(n.turn_id(), Some("turn_9")),
1565            other => panic!("expected Notification, got {other:?}"),
1566        }
1567    }
1568
1569    #[test]
1570    fn test_server_message_from_value_request() {
1571        let frame = serde_json::json!({
1572            "id": 7,
1573            "method": "execCommandApproval",
1574            "params": {
1575                "callId": "c1",
1576                "command": ["ls"],
1577                "conversationId": "th_1",
1578                "cwd": "/tmp",
1579                "parsedCmd": [],
1580            },
1581        });
1582        let msg = ServerMessage::from_value(frame).expect("parses a request frame");
1583        match msg {
1584            ServerMessage::Request { id, request } => {
1585                assert_eq!(id, RequestId::Integer(7));
1586                assert!(matches!(request, ServerRequest::ExecCommandApproval(_)));
1587            }
1588            other => panic!("expected Request, got {other:?}"),
1589        }
1590    }
1591
1592    #[test]
1593    fn test_server_message_from_json_str_rejects_response() {
1594        // A response to a client request is not a server-initiated message.
1595        let line = r#"{"id":1,"result":{"threadId":"th_abc"}}"#;
1596        let err = ServerMessage::from_json_str(line).unwrap_err();
1597        assert!(matches!(err, Error::Protocol(_)), "got: {err:?}");
1598    }
1599
1600    #[test]
1601    fn test_server_message_from_json_str_unknown_method_ok() {
1602        let line = r#"{"method":"some/future/notification","params":{"x":1}}"#;
1603        let msg = ServerMessage::from_json_str(line).unwrap();
1604        assert!(msg.is_unknown());
1605    }
1606}