1use crate::io::items::ThreadItem;
36use crate::jsonrpc::RequestId;
37use serde::{Deserialize, Serialize};
38use serde_json::Value;
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(tag = "type", rename_all = "camelCase")]
57pub enum UserInput {
58 Text { text: String },
60 Image { data: String },
62}
63
64#[derive(Debug, Clone, Default, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73pub struct ThreadStartParams {
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub instructions: Option<String>,
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub tools: Option<Vec<Value>>,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase")]
85pub struct ThreadStartResponse {
86 pub thread_id: String,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91#[serde(rename_all = "camelCase")]
92pub struct ThreadArchiveParams {
93 pub thread_id: String,
94}
95
96#[derive(Debug, Clone, Default, Serialize, Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub struct ThreadArchiveResponse {}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
110#[serde(rename_all = "camelCase")]
111pub struct TurnStartParams {
112 pub thread_id: String,
114 pub input: Vec<UserInput>,
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub model: Option<String>,
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub reasoning_effort: Option<String>,
122 #[serde(skip_serializing_if = "Option::is_none")]
124 pub sandbox_policy: Option<Value>,
125}
126
127#[derive(Debug, Clone, Default, Serialize, Deserialize)]
129#[serde(rename_all = "camelCase")]
130pub struct TurnStartResponse {}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135pub struct TurnInterruptParams {
136 pub thread_id: String,
137}
138
139#[derive(Debug, Clone, Default, Serialize, Deserialize)]
141#[serde(rename_all = "camelCase")]
142pub struct TurnInterruptResponse {}
143
144#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150#[serde(rename_all = "camelCase")]
151pub enum TurnStatus {
152 Completed,
154 Interrupted,
156 Failed,
158 InProgress,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
164#[serde(rename_all = "camelCase")]
165pub struct TurnError {
166 pub message: String,
167 #[serde(skip_serializing_if = "Option::is_none")]
168 pub codex_error_info: Option<Value>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
175#[serde(rename_all = "camelCase")]
176pub struct Turn {
177 pub id: String,
179 #[serde(default)]
181 pub items: Vec<ThreadItem>,
182 pub status: TurnStatus,
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub error: Option<TurnError>,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
197#[serde(rename_all = "camelCase")]
198pub struct TokenUsage {
199 pub input_tokens: u64,
201 pub output_tokens: u64,
203 #[serde(default)]
205 pub cached_input_tokens: u64,
206}
207
208#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214#[serde(rename_all = "camelCase")]
215pub enum ThreadStatus {
216 NotLoaded,
218 Idle,
220 Active,
222 SystemError,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
232#[serde(rename_all = "camelCase")]
233pub struct ThreadStartedNotification {
234 pub thread_id: String,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize)]
239#[serde(rename_all = "camelCase")]
240pub struct ThreadStatusChangedNotification {
241 pub thread_id: String,
242 pub status: ThreadStatus,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
247#[serde(rename_all = "camelCase")]
248pub struct TurnStartedNotification {
249 pub thread_id: String,
250 pub turn_id: String,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
255#[serde(rename_all = "camelCase")]
256pub struct TurnCompletedNotification {
257 pub thread_id: String,
258 pub turn_id: String,
259 pub turn: Turn,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
264#[serde(rename_all = "camelCase")]
265pub struct ItemStartedNotification {
266 pub thread_id: String,
267 pub turn_id: String,
268 pub item: ThreadItem,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
273#[serde(rename_all = "camelCase")]
274pub struct ItemCompletedNotification {
275 pub thread_id: String,
276 pub turn_id: String,
277 pub item: ThreadItem,
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize)]
282#[serde(rename_all = "camelCase")]
283pub struct AgentMessageDeltaNotification {
284 pub thread_id: String,
285 pub item_id: String,
286 pub delta: String,
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
291#[serde(rename_all = "camelCase")]
292pub struct CmdOutputDeltaNotification {
293 pub thread_id: String,
294 pub item_id: String,
295 pub delta: String,
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize)]
300#[serde(rename_all = "camelCase")]
301pub struct FileChangeOutputDeltaNotification {
302 pub thread_id: String,
303 pub item_id: String,
304 pub delta: String,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
309#[serde(rename_all = "camelCase")]
310pub struct ReasoningDeltaNotification {
311 pub thread_id: String,
312 pub item_id: String,
313 pub delta: String,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize)]
318#[serde(rename_all = "camelCase")]
319pub struct ErrorNotification {
320 pub error: String,
321 #[serde(skip_serializing_if = "Option::is_none")]
322 pub thread_id: Option<String>,
323 #[serde(skip_serializing_if = "Option::is_none")]
324 pub turn_id: Option<String>,
325 #[serde(default)]
326 pub will_retry: bool,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
331#[serde(rename_all = "camelCase")]
332pub struct ThreadTokenUsageUpdatedNotification {
333 pub thread_id: String,
334 pub usage: TokenUsage,
335}
336
337#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
346#[serde(rename_all = "camelCase")]
347pub enum CommandApprovalDecision {
348 Accept,
350 AcceptForSession,
352 Decline,
354 Cancel,
356}
357
358#[derive(Debug, Clone, Serialize, Deserialize)]
364#[serde(rename_all = "camelCase")]
365pub struct CommandExecutionApprovalParams {
366 pub thread_id: String,
367 pub turn_id: String,
368 pub call_id: String,
370 pub command: String,
372 pub cwd: String,
374 #[serde(skip_serializing_if = "Option::is_none")]
376 pub reason: Option<String>,
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize)]
381#[serde(rename_all = "camelCase")]
382pub struct CommandExecutionApprovalResponse {
383 pub decision: CommandApprovalDecision,
384}
385
386#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
391#[serde(rename_all = "camelCase")]
392pub enum FileChangeApprovalDecision {
393 Accept,
395 AcceptForSession,
397 Decline,
399 Cancel,
401}
402
403#[derive(Debug, Clone, Serialize, Deserialize)]
409#[serde(rename_all = "camelCase")]
410pub struct FileChangeApprovalParams {
411 pub thread_id: String,
412 pub turn_id: String,
413 pub call_id: String,
415 pub changes: Value,
417 #[serde(skip_serializing_if = "Option::is_none")]
419 pub reason: Option<String>,
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize)]
424#[serde(rename_all = "camelCase")]
425pub struct FileChangeApprovalResponse {
426 pub decision: FileChangeApprovalDecision,
427}
428
429#[derive(Debug, Clone)]
446pub enum ServerMessage {
447 Notification {
449 method: String,
450 params: Option<Value>,
451 },
452 Request {
455 id: RequestId,
456 method: String,
457 params: Option<Value>,
458 },
459}
460
461pub mod methods {
470 pub const THREAD_START: &str = "thread/start";
472 pub const THREAD_ARCHIVE: &str = "thread/archive";
473 pub const TURN_START: &str = "turn/start";
474 pub const TURN_INTERRUPT: &str = "turn/interrupt";
475 pub const TURN_STEER: &str = "turn/steer";
476
477 pub const THREAD_STARTED: &str = "thread/started";
479 pub const THREAD_STATUS_CHANGED: &str = "thread/status/changed";
480 pub const THREAD_TOKEN_USAGE_UPDATED: &str = "thread/tokenUsage/updated";
481 pub const TURN_STARTED: &str = "turn/started";
482 pub const TURN_COMPLETED: &str = "turn/completed";
483 pub const ITEM_STARTED: &str = "item/started";
484 pub const ITEM_COMPLETED: &str = "item/completed";
485 pub const AGENT_MESSAGE_DELTA: &str = "item/agentMessage/delta";
486 pub const CMD_OUTPUT_DELTA: &str = "item/commandExecution/outputDelta";
487 pub const FILE_CHANGE_OUTPUT_DELTA: &str = "item/fileChange/outputDelta";
488 pub const REASONING_DELTA: &str = "item/reasoning/summaryTextDelta";
489 pub const ERROR: &str = "error";
490
491 pub const CMD_EXEC_APPROVAL: &str = "item/commandExecution/requestApproval";
493 pub const FILE_CHANGE_APPROVAL: &str = "item/fileChange/requestApproval";
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499
500 #[test]
501 fn test_user_input_text() {
502 let input = UserInput::Text {
503 text: "Hello".to_string(),
504 };
505 let json = serde_json::to_string(&input).unwrap();
506 assert!(json.contains(r#""type":"text""#));
507 let parsed: UserInput = serde_json::from_str(&json).unwrap();
508 assert!(matches!(parsed, UserInput::Text { text } if text == "Hello"));
509 }
510
511 #[test]
512 fn test_thread_start_params() {
513 let params = ThreadStartParams {
514 instructions: Some("Be helpful".to_string()),
515 tools: None,
516 };
517 let json = serde_json::to_string(¶ms).unwrap();
518 assert!(json.contains("instructions"));
519 assert!(!json.contains("tools"));
520 }
521
522 #[test]
523 fn test_thread_start_response() {
524 let json = r#"{"threadId":"th_abc123"}"#;
525 let resp: ThreadStartResponse = serde_json::from_str(json).unwrap();
526 assert_eq!(resp.thread_id, "th_abc123");
527 }
528
529 #[test]
530 fn test_turn_start_params() {
531 let params = TurnStartParams {
532 thread_id: "th_1".to_string(),
533 input: vec![UserInput::Text {
534 text: "What is 2+2?".to_string(),
535 }],
536 model: None,
537 reasoning_effort: None,
538 sandbox_policy: None,
539 };
540 let json = serde_json::to_string(¶ms).unwrap();
541 assert!(json.contains("threadId"));
542 assert!(json.contains("input"));
543 }
544
545 #[test]
546 fn test_turn_status() {
547 let json = r#""completed""#;
548 let status: TurnStatus = serde_json::from_str(json).unwrap();
549 assert_eq!(status, TurnStatus::Completed);
550 }
551
552 #[test]
553 fn test_turn_completed_notification() {
554 let json = r#"{
555 "threadId": "th_1",
556 "turnId": "t_1",
557 "turn": {
558 "id": "t_1",
559 "items": [],
560 "status": "completed"
561 }
562 }"#;
563 let notif: TurnCompletedNotification = serde_json::from_str(json).unwrap();
564 assert_eq!(notif.thread_id, "th_1");
565 assert_eq!(notif.turn.status, TurnStatus::Completed);
566 }
567
568 #[test]
569 fn test_agent_message_delta() {
570 let json = r#"{"threadId":"th_1","itemId":"msg_1","delta":"Hello "}"#;
571 let notif: AgentMessageDeltaNotification = serde_json::from_str(json).unwrap();
572 assert_eq!(notif.delta, "Hello ");
573 }
574
575 #[test]
576 fn test_command_approval_decision() {
577 let json = r#""accept""#;
578 let decision: CommandApprovalDecision = serde_json::from_str(json).unwrap();
579 assert_eq!(decision, CommandApprovalDecision::Accept);
580
581 let json = r#""acceptForSession""#;
582 let decision: CommandApprovalDecision = serde_json::from_str(json).unwrap();
583 assert_eq!(decision, CommandApprovalDecision::AcceptForSession);
584 }
585
586 #[test]
587 fn test_command_approval_params() {
588 let json = r#"{
589 "threadId": "th_1",
590 "turnId": "t_1",
591 "callId": "call_1",
592 "command": "rm -rf /tmp/test",
593 "cwd": "/home/user"
594 }"#;
595 let params: CommandExecutionApprovalParams = serde_json::from_str(json).unwrap();
596 assert_eq!(params.command, "rm -rf /tmp/test");
597 }
598
599 #[test]
600 fn test_error_notification() {
601 let json = r#"{"error":"something failed","willRetry":true}"#;
602 let notif: ErrorNotification = serde_json::from_str(json).unwrap();
603 assert_eq!(notif.error, "something failed");
604 assert!(notif.will_retry);
605 }
606
607 #[test]
608 fn test_thread_status() {
609 let json = r#""idle""#;
610 let status: ThreadStatus = serde_json::from_str(json).unwrap();
611 assert_eq!(status, ThreadStatus::Idle);
612 }
613
614 #[test]
615 fn test_token_usage() {
616 let json = r#"{"inputTokens":100,"outputTokens":200,"cachedInputTokens":50}"#;
617 let usage: TokenUsage = serde_json::from_str(json).unwrap();
618 assert_eq!(usage.input_tokens, 100);
619 assert_eq!(usage.output_tokens, 200);
620 assert_eq!(usage.cached_input_tokens, 50);
621 }
622}