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