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;
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)))
}