Skip to main content

codex_codes/
protocol.rs

1//! App-server v2 protocol types for the Codex CLI.
2//!
3//! These types represent the JSON-RPC request parameters, response payloads,
4//! and notification bodies used by `codex app-server`. All wire types use
5//! camelCase field names via `#[serde(rename_all = "camelCase")]`.
6//!
7//! # Organization
8//!
9//! - **Request/Response pairs** — [`ThreadStartParams`]/[`ThreadStartResponse`],
10//!   [`TurnStartParams`]/[`TurnStartResponse`], etc.
11//! - **Server notifications** — Structs like [`TurnCompletedNotification`],
12//!   [`AgentMessageDeltaNotification`] that can be deserialized from the `params`
13//!   field of a [`ServerMessage::Notification`]
14//! - **Approval flow types** — [`CommandExecutionApprovalParams`] and
15//!   [`FileChangeApprovalParams`] for server-to-client requests that need a response
16//! - **Method constants** — The [`methods`] module contains all JSON-RPC method
17//!   name strings
18//!
19//! # Parsing notifications
20//!
21//! ```
22//! use codex_codes::protocol::{methods, TurnCompletedNotification};
23//! use serde_json::Value;
24//!
25//! fn handle_notification(method: &str, params: Option<Value>) {
26//!     if method == methods::TURN_COMPLETED {
27//!         if let Some(p) = params {
28//!             let notif: TurnCompletedNotification = serde_json::from_value(p).unwrap();
29//!             println!("Turn {} completed", notif.turn_id);
30//!         }
31//!     }
32//! }
33//! ```
34
35use crate::io::items::ThreadItem;
36use crate::jsonrpc::RequestId;
37use serde::{Deserialize, Serialize};
38use serde_json::Value;
39
40// ---------------------------------------------------------------------------
41// User input
42// ---------------------------------------------------------------------------
43
44/// User input sent as part of a [`TurnStartParams`].
45///
46/// # Example
47///
48/// ```
49/// use codex_codes::UserInput;
50///
51/// let text = UserInput::Text { text: "What is 2+2?".into() };
52/// let json = serde_json::to_string(&text).unwrap();
53/// assert!(json.contains(r#""type":"text""#));
54/// ```
55#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(tag = "type", rename_all = "camelCase")]
57pub enum UserInput {
58    /// Text input from the user.
59    Text { text: String },
60    /// Pre-encoded image as a data URI (e.g., `data:image/png;base64,...`).
61    Image { data: String },
62}
63
64// ---------------------------------------------------------------------------
65// Initialization handshake
66// ---------------------------------------------------------------------------
67
68/// Client info sent during the `initialize` handshake.
69///
70/// Identifies the connecting client to the app-server.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73pub struct ClientInfo {
74    /// Client application name (e.g., `"my-codex-app"`).
75    pub name: String,
76    /// Client version string (e.g., `"1.0.0"`).
77    pub version: String,
78    /// Human-readable display name.
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub title: Option<String>,
81}
82
83/// Client capabilities negotiated during `initialize`.
84#[derive(Debug, Clone, Default, Serialize, Deserialize)]
85#[serde(rename_all = "camelCase")]
86pub struct InitializeCapabilities {
87    /// Opt into receiving experimental API methods and fields.
88    #[serde(default)]
89    pub experimental_api: bool,
90    /// Notification method names to suppress for this connection.
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub opt_out_notification_methods: Option<Vec<String>>,
93}
94
95/// Parameters for the `initialize` request.
96///
97/// Must be the first request sent after connecting to the app-server.
98#[derive(Debug, Clone, Serialize, Deserialize)]
99#[serde(rename_all = "camelCase")]
100pub struct InitializeParams {
101    /// Information about the connecting client.
102    pub client_info: ClientInfo,
103    /// Optional client capabilities.
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub capabilities: Option<InitializeCapabilities>,
106}
107
108/// Response from the `initialize` request.
109#[derive(Debug, Clone, Serialize, Deserialize)]
110#[serde(rename_all = "camelCase")]
111pub struct InitializeResponse {
112    /// The server's user-agent string.
113    pub user_agent: String,
114}
115
116// ---------------------------------------------------------------------------
117// Thread lifecycle requests
118// ---------------------------------------------------------------------------
119
120/// Parameters for `thread/start`.
121///
122/// Use `ThreadStartParams::default()` for a basic thread with no custom instructions.
123#[derive(Debug, Clone, Default, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct ThreadStartParams {
126    /// Optional system instructions for the agent.
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub instructions: Option<String>,
129    /// Optional tool definitions to make available to the agent.
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub tools: Option<Vec<Value>>,
132}
133
134/// Thread metadata returned inside a [`ThreadStartResponse`].
135#[derive(Debug, Clone, Serialize, Deserialize)]
136#[serde(rename_all = "camelCase")]
137pub struct ThreadInfo {
138    /// Unique thread identifier.
139    pub id: String,
140    /// All other fields are captured but not typed.
141    #[serde(flatten)]
142    pub extra: Value,
143}
144
145/// Response from `thread/start`.
146#[derive(Debug, Clone, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub struct ThreadStartResponse {
149    /// The created thread.
150    pub thread: ThreadInfo,
151    /// The model assigned to this thread.
152    #[serde(default)]
153    pub model: Option<String>,
154    /// All other fields are captured but not typed.
155    #[serde(flatten)]
156    pub extra: Value,
157}
158
159impl ThreadStartResponse {
160    /// Convenience accessor for the thread ID.
161    pub fn thread_id(&self) -> &str {
162        &self.thread.id
163    }
164}
165
166/// Parameters for `thread/archive`.
167#[derive(Debug, Clone, Serialize, Deserialize)]
168#[serde(rename_all = "camelCase")]
169pub struct ThreadArchiveParams {
170    pub thread_id: String,
171}
172
173/// Response from `thread/archive`.
174#[derive(Debug, Clone, Default, Serialize, Deserialize)]
175#[serde(rename_all = "camelCase")]
176pub struct ThreadArchiveResponse {}
177
178// ---------------------------------------------------------------------------
179// Turn lifecycle requests
180// ---------------------------------------------------------------------------
181
182/// Parameters for `turn/start`.
183///
184/// Starts a new agent turn within an existing thread. The agent processes the
185/// input and streams notifications until the turn completes.
186#[derive(Debug, Clone, Serialize, Deserialize)]
187#[serde(rename_all = "camelCase")]
188pub struct TurnStartParams {
189    /// The thread ID from [`ThreadStartResponse`].
190    pub thread_id: String,
191    /// One or more user inputs (text and/or images).
192    pub input: Vec<UserInput>,
193    /// Override the model for this turn (e.g., `"o4-mini"`).
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub model: Option<String>,
196    /// Override reasoning effort for this turn (e.g., `"low"`, `"medium"`, `"high"`).
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub reasoning_effort: Option<String>,
199    /// Override sandbox policy for this turn.
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub sandbox_policy: Option<Value>,
202}
203
204/// Response from `turn/start`.
205#[derive(Debug, Clone, Default, Serialize, Deserialize)]
206#[serde(rename_all = "camelCase")]
207pub struct TurnStartResponse {}
208
209/// Parameters for `turn/interrupt`.
210#[derive(Debug, Clone, Serialize, Deserialize)]
211#[serde(rename_all = "camelCase")]
212pub struct TurnInterruptParams {
213    pub thread_id: String,
214}
215
216/// Response from `turn/interrupt`.
217#[derive(Debug, Clone, Default, Serialize, Deserialize)]
218#[serde(rename_all = "camelCase")]
219pub struct TurnInterruptResponse {}
220
221// ---------------------------------------------------------------------------
222// Turn status & data types
223// ---------------------------------------------------------------------------
224
225/// Status of a turn within a [`Turn`].
226#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
227#[serde(rename_all = "camelCase")]
228pub enum TurnStatus {
229    /// The agent finished normally.
230    Completed,
231    /// The turn was interrupted by the client via `turn/interrupt`.
232    Interrupted,
233    /// The turn failed with an error (see [`Turn::error`]).
234    Failed,
235    /// The turn is still being processed.
236    InProgress,
237}
238
239/// Error information from a failed turn.
240#[derive(Debug, Clone, Serialize, Deserialize)]
241#[serde(rename_all = "camelCase")]
242pub struct TurnError {
243    pub message: String,
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub codex_error_info: Option<Value>,
246}
247
248/// A completed turn with its items and final status.
249///
250/// Included in [`TurnCompletedNotification`] when a turn finishes.
251#[derive(Debug, Clone, Serialize, Deserialize)]
252#[serde(rename_all = "camelCase")]
253pub struct Turn {
254    /// Unique turn identifier.
255    pub id: String,
256    /// All items produced during this turn (messages, commands, file changes, etc.).
257    #[serde(default)]
258    pub items: Vec<ThreadItem>,
259    /// Final status of the turn.
260    pub status: TurnStatus,
261    /// Error details if `status` is [`TurnStatus::Failed`].
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub error: Option<TurnError>,
264}
265
266// ---------------------------------------------------------------------------
267// Token usage
268// ---------------------------------------------------------------------------
269
270/// Cumulative token usage for a thread.
271///
272/// Sent via [`ThreadTokenUsageUpdatedNotification`] after each turn.
273#[derive(Debug, Clone, Serialize, Deserialize)]
274#[serde(rename_all = "camelCase")]
275pub struct TokenUsage {
276    /// Total input tokens consumed.
277    pub input_tokens: u64,
278    /// Total output tokens generated.
279    pub output_tokens: u64,
280    /// Input tokens served from cache.
281    #[serde(default)]
282    pub cached_input_tokens: u64,
283}
284
285// ---------------------------------------------------------------------------
286// Thread status
287// ---------------------------------------------------------------------------
288
289/// Status of a thread, sent via [`ThreadStatusChangedNotification`].
290#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
291#[serde(rename_all = "camelCase")]
292pub enum ThreadStatus {
293    /// Thread is not yet loaded.
294    NotLoaded,
295    /// Thread is idle (no active turn).
296    Idle,
297    /// Thread has an active turn being processed.
298    Active,
299    /// Thread encountered an unrecoverable error.
300    SystemError,
301}
302
303// ---------------------------------------------------------------------------
304// Server notifications
305// ---------------------------------------------------------------------------
306
307/// `thread/started` notification.
308#[derive(Debug, Clone, Serialize, Deserialize)]
309#[serde(rename_all = "camelCase")]
310pub struct ThreadStartedNotification {
311    pub thread_id: String,
312}
313
314/// `thread/status/changed` notification.
315#[derive(Debug, Clone, Serialize, Deserialize)]
316#[serde(rename_all = "camelCase")]
317pub struct ThreadStatusChangedNotification {
318    pub thread_id: String,
319    pub status: ThreadStatus,
320}
321
322/// `turn/started` notification.
323#[derive(Debug, Clone, Serialize, Deserialize)]
324#[serde(rename_all = "camelCase")]
325pub struct TurnStartedNotification {
326    pub thread_id: String,
327    pub turn_id: String,
328}
329
330/// `turn/completed` notification.
331#[derive(Debug, Clone, Serialize, Deserialize)]
332#[serde(rename_all = "camelCase")]
333pub struct TurnCompletedNotification {
334    pub thread_id: String,
335    pub turn_id: String,
336    pub turn: Turn,
337}
338
339/// `item/started` notification.
340#[derive(Debug, Clone, Serialize, Deserialize)]
341#[serde(rename_all = "camelCase")]
342pub struct ItemStartedNotification {
343    pub thread_id: String,
344    pub turn_id: String,
345    pub item: ThreadItem,
346}
347
348/// `item/completed` notification.
349#[derive(Debug, Clone, Serialize, Deserialize)]
350#[serde(rename_all = "camelCase")]
351pub struct ItemCompletedNotification {
352    pub thread_id: String,
353    pub turn_id: String,
354    pub item: ThreadItem,
355}
356
357/// `item/agentMessage/delta` notification.
358#[derive(Debug, Clone, Serialize, Deserialize)]
359#[serde(rename_all = "camelCase")]
360pub struct AgentMessageDeltaNotification {
361    pub thread_id: String,
362    pub item_id: String,
363    pub delta: String,
364}
365
366/// `item/commandExecution/outputDelta` notification.
367#[derive(Debug, Clone, Serialize, Deserialize)]
368#[serde(rename_all = "camelCase")]
369pub struct CmdOutputDeltaNotification {
370    pub thread_id: String,
371    pub item_id: String,
372    pub delta: String,
373}
374
375/// `item/fileChange/outputDelta` notification.
376#[derive(Debug, Clone, Serialize, Deserialize)]
377#[serde(rename_all = "camelCase")]
378pub struct FileChangeOutputDeltaNotification {
379    pub thread_id: String,
380    pub item_id: String,
381    pub delta: String,
382}
383
384/// `item/reasoning/summaryTextDelta` notification.
385#[derive(Debug, Clone, Serialize, Deserialize)]
386#[serde(rename_all = "camelCase")]
387pub struct ReasoningDeltaNotification {
388    pub thread_id: String,
389    pub item_id: String,
390    pub delta: String,
391}
392
393/// `error` notification.
394#[derive(Debug, Clone, Serialize, Deserialize)]
395#[serde(rename_all = "camelCase")]
396pub struct ErrorNotification {
397    pub error: String,
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub thread_id: Option<String>,
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub turn_id: Option<String>,
402    #[serde(default)]
403    pub will_retry: bool,
404}
405
406/// `thread/tokenUsage/updated` notification.
407#[derive(Debug, Clone, Serialize, Deserialize)]
408#[serde(rename_all = "camelCase")]
409pub struct ThreadTokenUsageUpdatedNotification {
410    pub thread_id: String,
411    pub usage: TokenUsage,
412}
413
414// ---------------------------------------------------------------------------
415// Approval flow types (server-to-client requests)
416// ---------------------------------------------------------------------------
417
418/// Decision for command execution approval.
419///
420/// Sent as part of [`CommandExecutionApprovalResponse`] when responding to
421/// a command approval request from the server.
422#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
423#[serde(rename_all = "camelCase")]
424pub enum CommandApprovalDecision {
425    /// Allow this specific command to execute.
426    Accept,
427    /// Allow this command and similar future commands in this session.
428    AcceptForSession,
429    /// Reject this command.
430    Decline,
431    /// Cancel the entire turn.
432    Cancel,
433}
434
435/// Parameters for `item/commandExecution/requestApproval` (server → client).
436///
437/// The server sends this as a [`ServerMessage::Request`] when the agent wants
438/// to execute a command that requires user approval. Respond with
439/// [`CommandExecutionApprovalResponse`].
440#[derive(Debug, Clone, Serialize, Deserialize)]
441#[serde(rename_all = "camelCase")]
442pub struct CommandExecutionApprovalParams {
443    pub thread_id: String,
444    pub turn_id: String,
445    /// Unique identifier for this tool call.
446    pub call_id: String,
447    /// The shell command the agent wants to run.
448    pub command: String,
449    /// Working directory for the command.
450    pub cwd: String,
451    /// Human-readable explanation of why the command is needed.
452    #[serde(skip_serializing_if = "Option::is_none")]
453    pub reason: Option<String>,
454}
455
456/// Response for `item/commandExecution/requestApproval`.
457#[derive(Debug, Clone, Serialize, Deserialize)]
458#[serde(rename_all = "camelCase")]
459pub struct CommandExecutionApprovalResponse {
460    pub decision: CommandApprovalDecision,
461}
462
463/// Decision for file change approval.
464///
465/// Sent as part of [`FileChangeApprovalResponse`] when responding to
466/// a file change approval request from the server.
467#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
468#[serde(rename_all = "camelCase")]
469pub enum FileChangeApprovalDecision {
470    /// Allow this specific file change.
471    Accept,
472    /// Allow this and similar future file changes in this session.
473    AcceptForSession,
474    /// Reject this file change.
475    Decline,
476    /// Cancel the entire turn.
477    Cancel,
478}
479
480/// Parameters for `item/fileChange/requestApproval` (server → client).
481///
482/// The server sends this as a [`ServerMessage::Request`] when the agent wants
483/// to modify files and requires user approval. Respond with
484/// [`FileChangeApprovalResponse`].
485#[derive(Debug, Clone, Serialize, Deserialize)]
486#[serde(rename_all = "camelCase")]
487pub struct FileChangeApprovalParams {
488    pub thread_id: String,
489    pub turn_id: String,
490    /// Unique identifier for this tool call.
491    pub call_id: String,
492    /// The proposed file changes (structure varies by patch format).
493    pub changes: Value,
494    /// Human-readable explanation of why the changes are needed.
495    #[serde(skip_serializing_if = "Option::is_none")]
496    pub reason: Option<String>,
497}
498
499/// Response for `item/fileChange/requestApproval`.
500#[derive(Debug, Clone, Serialize, Deserialize)]
501#[serde(rename_all = "camelCase")]
502pub struct FileChangeApprovalResponse {
503    pub decision: FileChangeApprovalDecision,
504}
505
506// ---------------------------------------------------------------------------
507// Server message (what the client receives)
508// ---------------------------------------------------------------------------
509
510/// An incoming message from the app-server that the client should handle.
511///
512/// This is what [`AsyncClient::next_message`](crate::AsyncClient::next_message) and
513/// [`SyncClient::next_message`](crate::SyncClient::next_message) return.
514///
515/// # Handling
516///
517/// - **Notifications** are informational — no response is needed. Match on the `method`
518///   field and deserialize `params` into the appropriate notification type.
519/// - **Requests** require a response via `client.respond(id, &result)`. Currently
520///   used for approval flows (`item/commandExecution/requestApproval` and
521///   `item/fileChange/requestApproval`).
522#[derive(Debug, Clone)]
523pub enum ServerMessage {
524    /// A notification (no response needed). Deserialize `params` based on `method`.
525    Notification {
526        method: String,
527        params: Option<Value>,
528    },
529    /// A request from the server that needs a response (e.g., approval flow).
530    /// Use the client's `respond()` method with the `id`.
531    Request {
532        id: RequestId,
533        method: String,
534        params: Option<Value>,
535    },
536}
537
538// ---------------------------------------------------------------------------
539// Method name constants
540// ---------------------------------------------------------------------------
541
542/// JSON-RPC method names used by the app-server protocol.
543///
544/// Use these constants when matching on [`ServerMessage::Notification`] or
545/// [`ServerMessage::Request`] method fields to avoid typos.
546pub mod methods {
547    // Client → server requests
548    pub const INITIALIZE: &str = "initialize";
549    pub const INITIALIZED: &str = "initialized";
550    pub const THREAD_START: &str = "thread/start";
551    pub const THREAD_ARCHIVE: &str = "thread/archive";
552    pub const TURN_START: &str = "turn/start";
553    pub const TURN_INTERRUPT: &str = "turn/interrupt";
554    pub const TURN_STEER: &str = "turn/steer";
555
556    // Server → client notifications
557    pub const THREAD_STARTED: &str = "thread/started";
558    pub const THREAD_STATUS_CHANGED: &str = "thread/status/changed";
559    pub const THREAD_TOKEN_USAGE_UPDATED: &str = "thread/tokenUsage/updated";
560    pub const TURN_STARTED: &str = "turn/started";
561    pub const TURN_COMPLETED: &str = "turn/completed";
562    pub const ITEM_STARTED: &str = "item/started";
563    pub const ITEM_COMPLETED: &str = "item/completed";
564    pub const AGENT_MESSAGE_DELTA: &str = "item/agentMessage/delta";
565    pub const CMD_OUTPUT_DELTA: &str = "item/commandExecution/outputDelta";
566    pub const FILE_CHANGE_OUTPUT_DELTA: &str = "item/fileChange/outputDelta";
567    pub const REASONING_DELTA: &str = "item/reasoning/summaryTextDelta";
568    pub const ERROR: &str = "error";
569
570    // Server → client requests (approval)
571    pub const CMD_EXEC_APPROVAL: &str = "item/commandExecution/requestApproval";
572    pub const FILE_CHANGE_APPROVAL: &str = "item/fileChange/requestApproval";
573}
574
575#[cfg(test)]
576mod tests {
577    use super::*;
578
579    #[test]
580    fn test_initialize_params() {
581        let params = InitializeParams {
582            client_info: ClientInfo {
583                name: "my-app".to_string(),
584                version: "1.0.0".to_string(),
585                title: Some("My App".to_string()),
586            },
587            capabilities: None,
588        };
589        let json = serde_json::to_string(&params).unwrap();
590        assert!(json.contains("clientInfo"));
591        assert!(json.contains("my-app"));
592        assert!(!json.contains("capabilities"));
593    }
594
595    #[test]
596    fn test_initialize_response() {
597        let json = r#"{"userAgent":"codex-cli/0.104.0"}"#;
598        let resp: InitializeResponse = serde_json::from_str(json).unwrap();
599        assert_eq!(resp.user_agent, "codex-cli/0.104.0");
600    }
601
602    #[test]
603    fn test_initialize_capabilities() {
604        let params = InitializeParams {
605            client_info: ClientInfo {
606                name: "test".to_string(),
607                version: "0.1.0".to_string(),
608                title: None,
609            },
610            capabilities: Some(InitializeCapabilities {
611                experimental_api: true,
612                opt_out_notification_methods: Some(vec!["thread/started".to_string()]),
613            }),
614        };
615        let json = serde_json::to_string(&params).unwrap();
616        assert!(json.contains("experimentalApi"));
617        assert!(json.contains("optOutNotificationMethods"));
618    }
619
620    #[test]
621    fn test_user_input_text() {
622        let input = UserInput::Text {
623            text: "Hello".to_string(),
624        };
625        let json = serde_json::to_string(&input).unwrap();
626        assert!(json.contains(r#""type":"text""#));
627        let parsed: UserInput = serde_json::from_str(&json).unwrap();
628        assert!(matches!(parsed, UserInput::Text { text } if text == "Hello"));
629    }
630
631    #[test]
632    fn test_thread_start_params() {
633        let params = ThreadStartParams {
634            instructions: Some("Be helpful".to_string()),
635            tools: None,
636        };
637        let json = serde_json::to_string(&params).unwrap();
638        assert!(json.contains("instructions"));
639        assert!(!json.contains("tools"));
640    }
641
642    #[test]
643    fn test_thread_start_response() {
644        let json = r#"{"thread":{"id":"th_abc123"},"model":"gpt-4","approvalPolicy":"never","cwd":"/tmp","modelProvider":"openai","sandbox":{}}"#;
645        let resp: ThreadStartResponse = serde_json::from_str(json).unwrap();
646        assert_eq!(resp.thread_id(), "th_abc123");
647        assert_eq!(resp.model.as_deref(), Some("gpt-4"));
648    }
649
650    #[test]
651    fn test_turn_start_params() {
652        let params = TurnStartParams {
653            thread_id: "th_1".to_string(),
654            input: vec![UserInput::Text {
655                text: "What is 2+2?".to_string(),
656            }],
657            model: None,
658            reasoning_effort: None,
659            sandbox_policy: None,
660        };
661        let json = serde_json::to_string(&params).unwrap();
662        assert!(json.contains("threadId"));
663        assert!(json.contains("input"));
664    }
665
666    #[test]
667    fn test_turn_status() {
668        let json = r#""completed""#;
669        let status: TurnStatus = serde_json::from_str(json).unwrap();
670        assert_eq!(status, TurnStatus::Completed);
671    }
672
673    #[test]
674    fn test_turn_completed_notification() {
675        let json = r#"{
676            "threadId": "th_1",
677            "turnId": "t_1",
678            "turn": {
679                "id": "t_1",
680                "items": [],
681                "status": "completed"
682            }
683        }"#;
684        let notif: TurnCompletedNotification = serde_json::from_str(json).unwrap();
685        assert_eq!(notif.thread_id, "th_1");
686        assert_eq!(notif.turn.status, TurnStatus::Completed);
687    }
688
689    #[test]
690    fn test_agent_message_delta() {
691        let json = r#"{"threadId":"th_1","itemId":"msg_1","delta":"Hello "}"#;
692        let notif: AgentMessageDeltaNotification = serde_json::from_str(json).unwrap();
693        assert_eq!(notif.delta, "Hello ");
694    }
695
696    #[test]
697    fn test_command_approval_decision() {
698        let json = r#""accept""#;
699        let decision: CommandApprovalDecision = serde_json::from_str(json).unwrap();
700        assert_eq!(decision, CommandApprovalDecision::Accept);
701
702        let json = r#""acceptForSession""#;
703        let decision: CommandApprovalDecision = serde_json::from_str(json).unwrap();
704        assert_eq!(decision, CommandApprovalDecision::AcceptForSession);
705    }
706
707    #[test]
708    fn test_command_approval_params() {
709        let json = r#"{
710            "threadId": "th_1",
711            "turnId": "t_1",
712            "callId": "call_1",
713            "command": "rm -rf /tmp/test",
714            "cwd": "/home/user"
715        }"#;
716        let params: CommandExecutionApprovalParams = serde_json::from_str(json).unwrap();
717        assert_eq!(params.command, "rm -rf /tmp/test");
718    }
719
720    #[test]
721    fn test_error_notification() {
722        let json = r#"{"error":"something failed","willRetry":true}"#;
723        let notif: ErrorNotification = serde_json::from_str(json).unwrap();
724        assert_eq!(notif.error, "something failed");
725        assert!(notif.will_retry);
726    }
727
728    #[test]
729    fn test_thread_status() {
730        let json = r#""idle""#;
731        let status: ThreadStatus = serde_json::from_str(json).unwrap();
732        assert_eq!(status, ThreadStatus::Idle);
733    }
734
735    #[test]
736    fn test_token_usage() {
737        let json = r#"{"inputTokens":100,"outputTokens":200,"cachedInputTokens":50}"#;
738        let usage: TokenUsage = serde_json::from_str(json).unwrap();
739        assert_eq!(usage.input_tokens, 100);
740        assert_eq!(usage.output_tokens, 200);
741        assert_eq!(usage.cached_input_tokens, 50);
742    }
743}