codex-helper-core 0.19.0

Core library for codex-helper.
Documentation
use serde::{Deserialize, Serialize};

use crate::usage::UsageMetrics;

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum FleetNodeKind {
    #[default]
    Local,
    Remote,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum FleetNodeHealth {
    #[default]
    Fresh,
    Stale,
    AuthFailed,
    RateLimited,
    Unsupported,
    Unreachable,
    ParseFailed,
}

impl FleetNodeHealth {
    pub fn is_current(self) -> bool {
        matches!(self, FleetNodeHealth::Fresh)
    }
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum FleetWorkUnitKind {
    #[default]
    Root,
    Subagent,
    Process,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum FleetWorkUnitState {
    #[default]
    Unknown,
    Running,
    WaitingInput,
    WaitingApproval,
    Idle,
    Interrupted,
    Completed,
    Errored,
    Exited,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum FleetEvidenceSource {
    RuntimeStatus,
    SessionLog,
    ProcessScan,
    CachedSnapshot,
    #[default]
    Unavailable,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum FleetConfidence {
    High,
    Medium,
    Low,
    #[default]
    Unknown,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FleetEvidence {
    pub source: FleetEvidenceSource,
    pub confidence: FleetConfidence,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub detail: Option<String>,
}

impl FleetEvidence {
    pub fn high(source: FleetEvidenceSource) -> Self {
        Self {
            source,
            confidence: FleetConfidence::High,
            detail: None,
        }
    }

    pub fn with_detail(
        source: FleetEvidenceSource,
        confidence: FleetConfidence,
        detail: impl Into<String>,
    ) -> Self {
        Self {
            source,
            confidence,
            detail: Some(detail.into()),
        }
    }
}

impl Default for FleetEvidence {
    fn default() -> Self {
        Self {
            source: FleetEvidenceSource::Unavailable,
            confidence: FleetConfidence::Unknown,
            detail: None,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct FleetUsageSummary {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub last_usage: Option<UsageMetrics>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub total_usage: Option<UsageMetrics>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub turns_total: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub turns_with_usage: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub last_output_tokens_per_second: Option<f64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub avg_output_tokens_per_second: Option<f64>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FleetWorkUnit {
    pub node_id: String,
    pub id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub parent_id: Option<String>,
    pub kind: FleetWorkUnitKind,
    pub state: FleetWorkUnitState,
    pub evidence: FleetEvidence,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub session_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub local_thread_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub task_name: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cwd: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub model: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub station_name: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub provider_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub last_status: Option<u16>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub active_started_at_ms: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub last_activity_ms: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub last_error: Option<String>,
    #[serde(default)]
    pub usage: FleetUsageSummary,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum FleetGraphStatus {
    Available,
    #[default]
    Unavailable,
    Partial,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum FleetEdgeStatus {
    Open,
    Closed,
    #[default]
    Unknown,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FleetSubagentEdge {
    pub node_id: String,
    pub parent_id: String,
    pub child_id: String,
    pub status: FleetEdgeStatus,
    pub evidence: FleetEvidence,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct FleetTopology {
    pub status: FleetGraphStatus,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub edges: Vec<FleetSubagentEdge>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub note: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct FleetProcessSummary {
    #[serde(default)]
    pub scan_available: bool,
    #[serde(default)]
    pub codex_like_processes: usize,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FleetNodeSnapshot {
    pub node_id: String,
    pub label: String,
    pub kind: FleetNodeKind,
    pub health: FleetNodeHealth,
    pub refreshed_at_ms: u64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub stale_since_ms: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub snapshot_age_ms: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub active_endpoint: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub last_error: Option<String>,
    pub processes: FleetProcessSummary,
    pub topology: FleetTopology,
    #[serde(default)]
    pub work_units: Vec<FleetWorkUnit>,
}

impl FleetNodeSnapshot {
    pub fn current_work_units(&self) -> impl Iterator<Item = &FleetWorkUnit> {
        self.work_units.iter().filter(|unit| {
            matches!(
                unit.state,
                FleetWorkUnitState::Running
                    | FleetWorkUnitState::WaitingInput
                    | FleetWorkUnitState::WaitingApproval
            )
        })
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FleetSnapshot {
    pub api_version: u32,
    pub service_name: String,
    pub refreshed_at_ms: u64,
    #[serde(default)]
    pub nodes: Vec<FleetNodeSnapshot>,
}

impl FleetSnapshot {
    pub fn active_work_units(&self) -> usize {
        self.nodes
            .iter()
            .map(|node| node.current_work_units().count())
            .sum()
    }
}

pub fn now_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}