klieo-ops-api 3.7.0

Read-only HTTP monitoring router for klieo agentic systems.
Documentation
//! `GET /runs/:run_id/reconciliation` — ADR-055 Phase 1 read-model.

use std::sync::Arc;

use axum::extract::{Path, State};
use axum::Json;
use klieo_core::checkpoint::{RunCheckpoint, CHECKPOINT_BUCKET};
use klieo_core::ids::RunId;
use klieo_core::llm::ToolCall;
use klieo_core::KvStore;
use klieo_runlog::{build_worksheet, ReconciliationWorksheet};

use crate::error::OpsError;
use crate::state::OpsState;

/// Best-effort load of the tool calls that were actually dispatched on resume
/// and may have an uncertain outcome. Returns empty when: no `KvStore` is
/// wired, no checkpoint exists, the entry fails to deserialize, or the run
/// paused for approval but was never resumed (`resume_attempted == false`, so
/// its pending calls never fired and are not reconciliation candidates).
/// Backend/deserialize errors are logged before degrading to empty.
async fn pending_tool_calls(kv: Option<&dyn KvStore>, run_id: RunId) -> Vec<ToolCall> {
    let Some(kv) = kv else { return Vec::new() };
    let entry = match kv.get(CHECKPOINT_BUCKET, &run_id.to_string()).await {
        Ok(Some(entry)) => entry,
        Ok(None) => return Vec::new(),
        Err(e) => {
            tracing::warn!(operation = "pending_tool_calls", run_id = %run_id, detail = %e, "checkpoint read failed; worksheet may be incomplete");
            return Vec::new();
        }
    };
    let checkpoint = match serde_json::from_slice::<RunCheckpoint>(&entry.value) {
        Ok(checkpoint) => checkpoint,
        Err(e) => {
            tracing::warn!(operation = "pending_tool_calls", run_id = %run_id, detail = %e, "undeserializable checkpoint entry; worksheet may be incomplete");
            return Vec::new();
        }
    };
    if !checkpoint.resume_attempted {
        return Vec::new();
    }
    checkpoint.pending_tool_calls.unwrap_or_default()
}

pub(crate) async fn get_reconciliation(
    State(state): State<Arc<OpsState>>,
    Path(run_id_str): Path<String>,
) -> Result<Json<ReconciliationWorksheet>, OpsError> {
    let store = state
        .run_log_store
        .as_ref()
        .ok_or(OpsError::StoreNotConfigured)?;

    let run_id = ulid::Ulid::from_string(&run_id_str)
        .map(RunId)
        .map_err(|_| OpsError::RunNotFound)?;

    let log = store
        .get(run_id)
        .await
        .map_err(|e| {
            tracing::error!(operation = "get_reconciliation", run_id = %run_id_str, detail = %e, "store error");
            OpsError::Internal(Box::new(e))
        })?
        .ok_or(OpsError::RunNotFound)?;

    let pending = pending_tool_calls(state.kv_store.as_deref(), run_id).await;
    Ok(Json(build_worksheet(&log, &pending)))
}