kcode-codex-runtime-v2 0.1.2

Native dynamic-tool turns through the Codex app-server protocol
Documentation
use std::{
    fmt::{Debug, Formatter},
    time::Duration,
};

use serde_json::Value;

use crate::error::{Error, ErrorKind, Result};

/// Model reasoning effort accepted by Codex.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub enum ReasoningEffort {
    /// Disable reasoning when supported.
    None,
    /// Minimal reasoning.
    Minimal,
    /// Low reasoning.
    Low,
    /// Medium reasoning.
    Medium,
    /// High reasoning.
    High,
    /// Extra-high reasoning.
    #[default]
    XHigh,
    /// Maximum reasoning when supported.
    Max,
}

impl ReasoningEffort {
    /// Returns the Codex protocol value.
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::None => "none",
            Self::Minimal => "minimal",
            Self::Low => "low",
            Self::Medium => "medium",
            Self::High => "high",
            Self::XHigh => "xhigh",
            Self::Max => "max",
        }
    }
}

/// A supported image media type for an inline image turn.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ImageMediaType {
    /// Portable Network Graphics.
    Png,
    /// JPEG image data.
    Jpeg,
    /// WebP image data.
    Webp,
    /// Graphics Interchange Format.
    Gif,
}

impl ImageMediaType {
    /// Returns the media type used in the image data URL.
    pub const fn mime_type(self) -> &'static str {
        match self {
            Self::Png => "image/png",
            Self::Jpeg => "image/jpeg",
            Self::Webp => "image/webp",
            Self::Gif => "image/gif",
        }
    }
}

/// One in-memory image supplied to a dedicated image turn.
#[derive(Clone, Eq, PartialEq)]
pub struct ImageInput {
    media_type: ImageMediaType,
    bytes: Vec<u8>,
}

impl ImageInput {
    /// Constructs a nonempty image input.
    pub fn new(media_type: ImageMediaType, bytes: impl Into<Vec<u8>>) -> Result<Self> {
        let bytes = bytes.into();
        if bytes.is_empty() {
            return Err(Error::new(
                ErrorKind::InvalidInput,
                "Codex image input must not be empty",
            ));
        }
        Ok(Self { media_type, bytes })
    }

    /// Returns the declared image media type.
    pub const fn media_type(&self) -> ImageMediaType {
        self.media_type
    }

    /// Returns the exact image bytes.
    pub fn bytes(&self) -> &[u8] {
        &self.bytes
    }
}

impl Debug for ImageInput {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        formatter
            .debug_struct("ImageInput")
            .field("media_type", &self.media_type)
            .field("byte_len", &self.bytes.len())
            .finish()
    }
}

/// A fresh, ephemeral, tool-free Codex image-analysis turn.
#[derive(Clone, Debug, PartialEq)]
pub struct ImageTurnRequest {
    /// Exact text supplied after the image input items.
    pub prompt: String,
    /// Codex model identifier.
    pub model: String,
    /// Images supplied in caller order.
    pub images: Vec<ImageInput>,
    /// Requested reasoning effort.
    pub reasoning_effort: ReasoningEffort,
    /// Maximum duration of the complete turn.
    pub timeout: Duration,
}

impl ImageTurnRequest {
    /// Constructs an image turn using extra-high reasoning and a ten-minute timeout.
    pub fn new(
        prompt: impl Into<String>,
        model: impl Into<String>,
        images: Vec<ImageInput>,
    ) -> Self {
        Self {
            prompt: prompt.into(),
            model: model.into(),
            images,
            reasoning_effort: ReasoningEffort::XHigh,
            timeout: Duration::from_secs(10 * 60),
        }
    }
}

/// A dynamic function exposed to Codex for a fresh thread.
#[derive(Clone, Debug, PartialEq)]
pub struct DynamicTool {
    /// Function name visible to the model.
    pub name: String,
    /// Short function description visible to the model.
    pub description: String,
    /// JSON Schema for the function arguments.
    pub input_schema: Value,
}

impl DynamicTool {
    /// Constructs one function tool.
    pub fn new(
        name: impl Into<String>,
        description: impl Into<String>,
        input_schema: Value,
    ) -> Self {
        Self {
            name: name.into(),
            description: description.into(),
            input_schema,
        }
    }
}

/// One standard Codex turn request.
#[derive(Clone, Debug, PartialEq)]
pub struct AgentRequest {
    /// Exact text supplied as the user input item for this turn.
    pub input: String,
    /// Codex model identifier.
    pub model: String,
    /// Requested reasoning effort.
    pub reasoning_effort: ReasoningEffort,
    /// Existing Codex thread identifier to resume.
    pub previous_thread_id: Option<String>,
    /// Dynamic functions supplied when a fresh thread is created.
    pub tools: Vec<DynamicTool>,
    /// Whether a fresh thread avoids persistent Codex session storage.
    pub ephemeral: bool,
    /// Maximum duration of the complete turn, including tool handling.
    pub timeout: Duration,
}

impl AgentRequest {
    /// Constructs a fresh turn with no tools.
    pub fn new(input: impl Into<String>, model: impl Into<String>) -> Self {
        Self {
            input: input.into(),
            model: model.into(),
            reasoning_effort: ReasoningEffort::XHigh,
            previous_thread_id: None,
            tools: Vec::new(),
            ephemeral: false,
            timeout: Duration::from_secs(10 * 60),
        }
    }
}

/// A dynamic function call requested by Codex.
#[derive(Clone, Debug, PartialEq)]
pub struct DynamicToolCall {
    /// Protocol call identifier used when returning the result.
    pub call_id: String,
    /// Dynamic function name.
    pub tool: String,
    /// Parsed JSON arguments supplied by the model.
    pub arguments: Value,
}

/// Caller-provided result for one dynamic function call.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ToolResult {
    /// Whether the function completed successfully.
    pub success: bool,
    /// Text returned to the model.
    pub text: String,
}

impl ToolResult {
    /// Constructs a successful text result.
    pub fn success(text: impl Into<String>) -> Self {
        Self {
            success: true,
            text: text.into(),
        }
    }

    /// Constructs a failed text result for the model.
    pub fn failure(text: impl Into<String>) -> Self {
        Self {
            success: false,
            text: text.into(),
        }
    }
}

/// Normalized cumulative and latest-turn token accounting.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct TokenUsage {
    /// Cumulative input tokens for the thread.
    pub input_tokens: u64,
    /// Cumulative output tokens for the thread.
    pub output_tokens: u64,
    /// Cumulative cached input tokens for the thread.
    pub cached_input_tokens: u64,
    /// Cumulative reasoning output tokens for the thread.
    pub reasoning_output_tokens: u64,
    /// Input tokens for the latest model round.
    pub last_input_tokens: Option<u64>,
    /// Output tokens for the latest model round.
    pub last_output_tokens: Option<u64>,
}

/// Successful terminal state of one Codex turn.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CompletedTurn {
    /// Codex thread identifier used for continuation.
    pub thread_id: String,
    /// Codex turn identifier.
    pub turn_id: String,
    /// Terminal assistant text. It may be empty after a control tool call.
    pub answer: String,
    /// Latest provider token accounting, when reported.
    pub usage: Option<TokenUsage>,
}

/// Event yielded while a Codex turn is running.
#[derive(Clone, Debug, PartialEq)]
pub enum AgentEvent {
    /// Exact UTF-8 JSONL record written by the client to Codex, including its newline.
    ProviderInput(String),
    /// A dynamic function call requiring a response from the caller.
    ToolCall(DynamicToolCall),
    /// The turn completed successfully.
    Completed(CompletedTurn),
}