codex-cli-sdk 0.0.1

Rust SDK for the OpenAI Codex CLI
Documentation
use serde::{Deserialize, Serialize};
use serde_json::Value;

/// A typed item that occurs during a turn.
///
/// Items flow through the lifecycle: `ItemStarted` → `ItemUpdated` → `ItemCompleted`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ThreadItem {
    /// Agent text response.
    #[serde(rename = "agent_message")]
    AgentMessage {
        id: String,
        #[serde(default)]
        text: String,
    },

    /// Agent reasoning/thinking.
    #[serde(rename = "reasoning")]
    Reasoning {
        id: String,
        #[serde(default)]
        text: String,
    },

    /// Shell command execution.
    #[serde(rename = "command_execution")]
    CommandExecution {
        id: String,
        #[serde(default)]
        command: String,
        #[serde(default)]
        aggregated_output: String,
        #[serde(default)]
        exit_code: Option<i32>,
        #[serde(default)]
        status: CommandExecutionStatus,
    },

    /// File change (patch application).
    #[serde(rename = "file_change")]
    FileChange {
        id: String,
        #[serde(default)]
        changes: Vec<FileUpdateChange>,
        #[serde(default)]
        status: PatchApplyStatus,
    },

    /// MCP tool invocation.
    #[serde(rename = "mcp_tool_call")]
    McpToolCall {
        id: String,
        #[serde(default)]
        server: String,
        #[serde(default)]
        tool: String,
        #[serde(default)]
        arguments: Value,
        #[serde(default)]
        result: Option<McpToolCallResult>,
        #[serde(default)]
        error: Option<McpToolCallError>,
        #[serde(default)]
        status: McpToolCallStatus,
    },

    /// Web search invocation.
    #[serde(rename = "web_search")]
    WebSearch {
        id: String,
        #[serde(default)]
        query: String,
    },

    /// Agent-managed todo list.
    #[serde(rename = "todo_list")]
    TodoList {
        id: String,
        #[serde(default)]
        items: Vec<TodoItem>,
    },

    /// Error item.
    #[serde(rename = "error")]
    Error {
        id: String,
        #[serde(default)]
        message: String,
    },
}

impl ThreadItem {
    /// Get the item ID regardless of variant.
    pub fn id(&self) -> &str {
        match self {
            Self::AgentMessage { id, .. }
            | Self::Reasoning { id, .. }
            | Self::CommandExecution { id, .. }
            | Self::FileChange { id, .. }
            | Self::McpToolCall { id, .. }
            | Self::WebSearch { id, .. }
            | Self::TodoList { id, .. }
            | Self::Error { id, .. } => id,
        }
    }
}

// ── Supporting types ───────────────────────────────────────────

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CommandExecutionStatus {
    #[default]
    InProgress,
    Completed,
    Failed,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileUpdateChange {
    pub path: String,
    pub kind: PatchChangeKind,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PatchChangeKind {
    Add,
    Delete,
    Update,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PatchApplyStatus {
    #[default]
    Completed,
    Failed,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpToolCallResult {
    #[serde(default)]
    pub content: Vec<Value>,
    #[serde(default)]
    pub structured_content: Value,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpToolCallError {
    pub message: String,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum McpToolCallStatus {
    #[default]
    InProgress,
    Completed,
    Failed,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TodoItem {
    pub text: String,
    #[serde(default)]
    pub completed: bool,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn agent_message_round_trip() {
        let item = ThreadItem::AgentMessage {
            id: "msg-1".into(),
            text: "Hello".into(),
        };
        let json = serde_json::to_string(&item).unwrap();
        let parsed: ThreadItem = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.id(), "msg-1");
    }

    #[test]
    fn command_execution_defaults() {
        let json = r#"{"type":"command_execution","id":"cmd-1"}"#;
        let item: ThreadItem = serde_json::from_str(json).unwrap();
        let ThreadItem::CommandExecution {
            command, exit_code, ..
        } = item
        else {
            panic!("wrong variant");
        };
        assert_eq!(command, "");
        assert_eq!(exit_code, None);
    }

    #[test]
    fn mcp_tool_call_round_trip() {
        let json = r#"{"type":"mcp_tool_call","id":"mcp-1","server":"test","tool":"search","arguments":{},"status":"completed"}"#;
        let item: ThreadItem = serde_json::from_str(json).unwrap();
        assert_eq!(item.id(), "mcp-1");
    }

    #[test]
    fn todo_list_round_trip() {
        let json =
            r#"{"type":"todo_list","id":"todo-1","items":[{"text":"Do thing","completed":false}]}"#;
        let item: ThreadItem = serde_json::from_str(json).unwrap();
        let ThreadItem::TodoList { items, .. } = item else {
            panic!("wrong variant");
        };
        assert_eq!(items.len(), 1);
        assert_eq!(items[0].text, "Do thing");
        assert!(!items[0].completed);
    }
}