klieo-ops-api 3.4.0

Read-only HTTP monitoring router for klieo agentic systems.
Documentation
//! Response types for the `/_ops` monitoring endpoints.

use chrono::{DateTime, Utc};
use klieo_runlog::types::{Cost, RunStatus, Usage};
use serde::Serialize;

/// Compact per-run summary returned by `GET /runs`. Snapshot at fetch
/// time — may differ from the live store if a transition raced. Drops
/// `steps[]`; use `GET /runs/{id}` for full detail.
#[non_exhaustive]
#[derive(Debug, Clone, Serialize)]
pub struct RunLogSummary {
    /// `RunId` rendered to its JSON-safe string form (the wire shape).
    pub run_id: String,
    #[allow(missing_docs)]
    pub agent: String,
    #[allow(missing_docs)]
    pub status: RunStatus,
    /// UTC wall-clock at run creation; immutable post-fetch.
    pub started_at: DateTime<Utc>,
    /// When the run reached a terminal status; `None` while still running.
    pub finished_at: Option<DateTime<Utc>>,
    /// Aggregate prompt/completion token usage.
    pub tokens: Usage,
    /// Optional cost estimate aggregated from per-step `LlmCall` records.
    pub cost_estimate: Option<Cost>,
}

impl RunLogSummary {
    /// Drop a [`klieo_runlog::RunLog`]'s heavy `steps[]` and keep only
    /// the headline fields for list-page responses.
    pub fn from_run_log(log: &klieo_runlog::RunLog) -> Self {
        Self {
            run_id: log.run_id.to_string(),
            agent: log.agent.clone(),
            status: log.status,
            started_at: log.started_at,
            finished_at: log.finished_at,
            tokens: log.tokens,
            cost_estimate: log.cost_estimate,
        }
    }
}

/// Returned by `GET /_ops/agents`. Fields aggregated across all runs in
/// the store scan window. Counts are snapshots, not transactional
/// w.r.t. concurrent transitions.
#[non_exhaustive]
#[derive(Debug, Clone, Serialize)]
pub struct AgentSummary {
    /// Primary aggregation key; matches [`RunLogSummary::agent`].
    pub agent: String,
    /// Most recent `started_at` seen across this agent's runs.
    pub last_seen: DateTime<Utc>,
    #[allow(missing_docs)]
    pub runs_running: u32,
    #[allow(missing_docs)]
    pub runs_completed: u32,
    #[allow(missing_docs)]
    pub runs_failed: u32,
    #[allow(missing_docs)]
    pub runs_cancelled: u32,
}

/// Per-stream summary derived from `KvResumeBuffer`. Returned by `GET /streams`.
#[non_exhaustive]
#[derive(Debug, Clone, Serialize)]
pub struct StreamSummary {
    /// Stream identifier (the `KvResumeBuffer` bucket key).
    pub stream_id: String,
    /// `true` once the stream has emitted its terminal event.
    pub is_terminal: bool,
    /// Next event id to be assigned — a proxy for total events recorded.
    pub next_id: u64,
    /// Unix milliseconds of the most recent buffer write.
    pub last_write_unix_ms: u64,
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::Utc;
    use klieo_core::ids::RunId;
    use klieo_runlog::types::RunStatus;

    #[test]
    fn run_log_summary_serialises_run_id_as_string() {
        let run_id = RunId::new();
        let summary = RunLogSummary {
            run_id: run_id.to_string(),
            agent: "test-agent".into(),
            status: RunStatus::Running,
            started_at: Utc::now(),
            finished_at: None,
            tokens: Usage::default(),
            cost_estimate: None,
        };
        let json = serde_json::to_string(&summary).unwrap();
        assert!(
            json.contains(&format!("\"run_id\":\"{}\"", run_id)),
            "run_id not serialised as string, got: {json}"
        );
        assert!(json.contains("\"status\":\"running\""), "got: {json}");
        assert!(json.contains("\"agent\":\"test-agent\""), "got: {json}");
    }

    #[test]
    fn agent_summary_serialises() {
        let summary = AgentSummary {
            agent: "writer".into(),
            last_seen: Utc::now(),
            runs_running: 1,
            runs_completed: 5,
            runs_failed: 0,
            runs_cancelled: 0,
        };
        let json = serde_json::to_string(&summary).unwrap();
        assert!(json.contains("\"agent\":\"writer\""), "got: {json}");
        assert!(json.contains("\"runs_completed\":5"), "got: {json}");
    }

    #[test]
    fn stream_summary_serialises() {
        let summary = StreamSummary {
            stream_id: "s-001".into(),
            is_terminal: false,
            next_id: 7,
            last_write_unix_ms: 1_000_000,
        };
        let json = serde_json::to_string(&summary).unwrap();
        assert!(json.contains("\"stream_id\":\"s-001\""), "got: {json}");
        assert!(json.contains("\"next_id\":7"), "got: {json}");
    }
}