Skip to main content

klieo_ops_api/
types.rs

1//! Response types for the `/_ops` monitoring endpoints.
2
3use chrono::{DateTime, Utc};
4use klieo_runlog::types::{Cost, RunStatus, Usage};
5use serde::Serialize;
6
7/// Compact per-run summary returned by `GET /runs`. Snapshot at fetch
8/// time — may differ from the live store if a transition raced. Drops
9/// `steps[]`; use `GET /runs/{id}` for full detail.
10#[non_exhaustive]
11#[derive(Debug, Clone, Serialize)]
12pub struct RunLogSummary {
13    /// `RunId` rendered to its JSON-safe string form (the wire shape).
14    pub run_id: String,
15    #[allow(missing_docs)]
16    pub agent: String,
17    #[allow(missing_docs)]
18    pub status: RunStatus,
19    /// UTC wall-clock at run creation; immutable post-fetch.
20    pub started_at: DateTime<Utc>,
21    /// When the run reached a terminal status; `None` while still running.
22    pub finished_at: Option<DateTime<Utc>>,
23    /// Aggregate prompt/completion token usage.
24    pub tokens: Usage,
25    /// Optional cost estimate aggregated from per-step `LlmCall` records.
26    pub cost_estimate: Option<Cost>,
27}
28
29impl RunLogSummary {
30    /// Drop a [`klieo_runlog::RunLog`]'s heavy `steps[]` and keep only
31    /// the headline fields for list-page responses.
32    pub fn from_run_log(log: &klieo_runlog::RunLog) -> Self {
33        Self {
34            run_id: log.run_id.to_string(),
35            agent: log.agent.clone(),
36            status: log.status,
37            started_at: log.started_at,
38            finished_at: log.finished_at,
39            tokens: log.tokens,
40            cost_estimate: log.cost_estimate,
41        }
42    }
43}
44
45/// Returned by `GET /_ops/agents`. Fields aggregated across all runs in
46/// the store scan window. Counts are snapshots, not transactional
47/// w.r.t. concurrent transitions.
48#[non_exhaustive]
49#[derive(Debug, Clone, Serialize)]
50pub struct AgentSummary {
51    /// Primary aggregation key; matches [`RunLogSummary::agent`].
52    pub agent: String,
53    /// Most recent `started_at` seen across this agent's runs.
54    pub last_seen: DateTime<Utc>,
55    #[allow(missing_docs)]
56    pub runs_running: u32,
57    #[allow(missing_docs)]
58    pub runs_completed: u32,
59    #[allow(missing_docs)]
60    pub runs_failed: u32,
61    #[allow(missing_docs)]
62    pub runs_cancelled: u32,
63}
64
65/// Per-stream summary derived from `KvResumeBuffer`. Returned by `GET /streams`.
66#[non_exhaustive]
67#[derive(Debug, Clone, Serialize)]
68pub struct StreamSummary {
69    /// Stream identifier (the `KvResumeBuffer` bucket key).
70    pub stream_id: String,
71    /// `true` once the stream has emitted its terminal event.
72    pub is_terminal: bool,
73    /// Next event id to be assigned — a proxy for total events recorded.
74    pub next_id: u64,
75    /// Unix milliseconds of the most recent buffer write.
76    pub last_write_unix_ms: u64,
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use chrono::Utc;
83    use klieo_core::ids::RunId;
84    use klieo_runlog::types::RunStatus;
85
86    #[test]
87    fn run_log_summary_serialises_run_id_as_string() {
88        let run_id = RunId::new();
89        let summary = RunLogSummary {
90            run_id: run_id.to_string(),
91            agent: "test-agent".into(),
92            status: RunStatus::Running,
93            started_at: Utc::now(),
94            finished_at: None,
95            tokens: Usage::default(),
96            cost_estimate: None,
97        };
98        let json = serde_json::to_string(&summary).unwrap();
99        assert!(
100            json.contains(&format!("\"run_id\":\"{}\"", run_id)),
101            "run_id not serialised as string, got: {json}"
102        );
103        assert!(json.contains("\"status\":\"running\""), "got: {json}");
104        assert!(json.contains("\"agent\":\"test-agent\""), "got: {json}");
105    }
106
107    #[test]
108    fn agent_summary_serialises() {
109        let summary = AgentSummary {
110            agent: "writer".into(),
111            last_seen: Utc::now(),
112            runs_running: 1,
113            runs_completed: 5,
114            runs_failed: 0,
115            runs_cancelled: 0,
116        };
117        let json = serde_json::to_string(&summary).unwrap();
118        assert!(json.contains("\"agent\":\"writer\""), "got: {json}");
119        assert!(json.contains("\"runs_completed\":5"), "got: {json}");
120    }
121
122    #[test]
123    fn stream_summary_serialises() {
124        let summary = StreamSummary {
125            stream_id: "s-001".into(),
126            is_terminal: false,
127            next_id: 7,
128            last_write_unix_ms: 1_000_000,
129        };
130        let json = serde_json::to_string(&summary).unwrap();
131        assert!(json.contains("\"stream_id\":\"s-001\""), "got: {json}");
132        assert!(json.contains("\"next_id\":7"), "got: {json}");
133    }
134}