microclaw-engine 0.5.0

Channel-independent Agent Engine and default services for MicroClaw
Documentation
use std::sync::Arc;

use chrono::SecondsFormat;

use crate::internal::storage::db::{
    call_blocking, Database, ExperienceSummary, LlmModelUsageSummary, LlmUsageSummary,
    MemoryObservabilitySummary, SkillLearningSummary,
};

fn fmt_int(v: i64) -> String {
    let neg = v < 0;
    let mut n = v.unsigned_abs();
    let mut parts = Vec::new();
    while n >= 1000 {
        parts.push(format!("{:03}", n % 1000));
        n /= 1000;
    }
    let mut out = n.to_string();
    while let Some(p) = parts.pop() {
        out.push(',');
        out.push_str(&p);
    }
    if neg {
        format!("-{out}")
    } else {
        out
    }
}

fn fmt_summary_line(name: &str, s: &LlmUsageSummary) -> String {
    format!(
        "{name:<8} req={:>4}  tok={} (in {} / out {})",
        fmt_int(s.requests),
        fmt_int(s.total_tokens),
        fmt_int(s.input_tokens),
        fmt_int(s.output_tokens)
    )
}

fn format_model_rows(rows: &[LlmModelUsageSummary], max_rows: usize) -> Vec<String> {
    if rows.is_empty() {
        return vec!["    - (no data)".to_string()];
    }

    rows.iter()
        .take(max_rows)
        .enumerate()
        .map(|(idx, row)| {
            format!(
                "    {}. {}  tok={}  req={}  in {} / out {}",
                idx + 1,
                row.model,
                fmt_int(row.total_tokens),
                fmt_int(row.requests),
                fmt_int(row.input_tokens),
                fmt_int(row.output_tokens)
            )
        })
        .collect()
}

fn block_lines(
    title: &str,
    all: &LlmUsageSummary,
    d24: &LlmUsageSummary,
    d7: &LlmUsageSummary,
    models_24h: &[LlmModelUsageSummary],
    models_7d: &[LlmModelUsageSummary],
) -> Vec<String> {
    let mut lines = vec![
        title.to_string(),
        "".to_string(),
        format!("  🧮 {}", fmt_summary_line("All-time", all)),
        format!("  🕓 {}", fmt_summary_line("Last 24h", d24)),
        format!("  📆 {}", fmt_summary_line("Last 7d", d7)),
        "".to_string(),
        "  🤖 Top models (24h)".to_string(),
    ];
    lines.extend(format_model_rows(models_24h, 4));
    lines.push("".to_string());
    lines.push("  🤖 Top models (7d)".to_string());
    lines.extend(format_model_rows(models_7d, 4));

    lines
}

async fn query_summary(
    db: Arc<Database>,
    chat_id: Option<i64>,
    since: Option<String>,
) -> Result<LlmUsageSummary, String> {
    call_blocking(db, move |d| {
        d.get_llm_usage_summary_since(chat_id, since.as_deref())
    })
    .await
    .map_err(|e| e.to_string())
}

async fn query_by_model(
    db: Arc<Database>,
    chat_id: Option<i64>,
    since: Option<String>,
) -> Result<Vec<LlmModelUsageSummary>, String> {
    call_blocking(db, move |d| {
        d.get_llm_usage_by_model(chat_id, since.as_deref(), None)
    })
    .await
    .map_err(|e| e.to_string())
}

async fn query_memory_summary(
    db: Arc<Database>,
    chat_id: Option<i64>,
) -> Result<MemoryObservabilitySummary, String> {
    call_blocking(db, move |d| d.get_memory_observability_summary(chat_id))
        .await
        .map_err(|e| e.to_string())
}

async fn query_learning_summary(
    db: Arc<Database>,
    chat_id: Option<i64>,
) -> Result<ExperienceSummary, String> {
    call_blocking(db, move |d| d.get_experience_summary(chat_id))
        .await
        .map_err(|e| e.to_string())
}

async fn query_skill_learning(db: Arc<Database>) -> Result<Vec<SkillLearningSummary>, String> {
    call_blocking(db, move |d| d.get_skill_learning_summaries())
        .await
        .map_err(|e| e.to_string())
}

pub async fn build_usage_report(db: Arc<Database>, chat_id: i64) -> Result<String, String> {
    let now = chrono::Utc::now();
    let since_24h = (now - chrono::Duration::hours(24)).to_rfc3339();
    let since_7d = (now - chrono::Duration::days(7)).to_rfc3339();

    let chat_all = query_summary(db.clone(), Some(chat_id), None).await?;
    let chat_24h = query_summary(db.clone(), Some(chat_id), Some(since_24h.clone())).await?;
    let chat_7d = query_summary(db.clone(), Some(chat_id), Some(since_7d.clone())).await?;
    let chat_models_24h = query_by_model(db.clone(), Some(chat_id), Some(since_24h)).await?;
    let chat_models_7d = query_by_model(db.clone(), Some(chat_id), Some(since_7d)).await?;

    let global_all = query_summary(db.clone(), None, None).await?;
    let global_24h = query_summary(
        db.clone(),
        None,
        Some((now - chrono::Duration::hours(24)).to_rfc3339()),
    )
    .await?;
    let global_7d = query_summary(
        db.clone(),
        None,
        Some((now - chrono::Duration::days(7)).to_rfc3339()),
    )
    .await?;
    let global_models_24h = query_by_model(
        db.clone(),
        None,
        Some((now - chrono::Duration::hours(24)).to_rfc3339()),
    )
    .await?;
    let global_models_7d = query_by_model(
        db.clone(),
        None,
        Some((now - chrono::Duration::days(7)).to_rfc3339()),
    )
    .await?;
    let chat_mem = query_memory_summary(db.clone(), Some(chat_id)).await?;
    let global_mem = query_memory_summary(db.clone(), None).await?;
    let chat_learning = query_learning_summary(db.clone(), Some(chat_id)).await?;
    let global_learning = query_learning_summary(db.clone(), None).await?;
    let skill_learning = query_skill_learning(db).await?;

    let mut lines = vec![
        "📊 Token Usage".to_string(),
        format!(
            "🕒 Updated: {}",
            now.to_rfc3339_opts(SecondsFormat::Secs, true)
        ),
        "".to_string(),
    ];

    lines.extend(block_lines(
        "🔹 This chat",
        &chat_all,
        &chat_24h,
        &chat_7d,
        &chat_models_24h,
        &chat_models_7d,
    ));

    lines.push("".to_string());

    lines.extend(block_lines(
        "🌍 Global",
        &global_all,
        &global_24h,
        &global_7d,
        &global_models_24h,
        &global_models_7d,
    ));

    lines.push("".to_string());
    lines.push("🧠 Memory Observability".to_string());
    lines.push("".to_string());
    lines.push(format!(
        "  This chat: total={} active={} archived={} avg_conf={:.2} low_conf={}",
        fmt_int(chat_mem.total),
        fmt_int(chat_mem.active),
        fmt_int(chat_mem.archived),
        chat_mem.avg_confidence,
        fmt_int(chat_mem.low_confidence)
    ));
    lines.push(format!(
        "  Reflector 24h: runs={} +{} ~{} -{}",
        fmt_int(chat_mem.reflector_runs_24h),
        fmt_int(chat_mem.reflector_inserted_24h),
        fmt_int(chat_mem.reflector_updated_24h),
        fmt_int(chat_mem.reflector_skipped_24h)
    ));
    lines.push(format!(
        "  Injection 24h: events={} selected/candidates={}/{}",
        fmt_int(chat_mem.injection_events_24h),
        fmt_int(chat_mem.injection_selected_24h),
        fmt_int(chat_mem.injection_candidates_24h)
    ));
    lines.push("".to_string());
    lines.push(format!(
        "  Global: total={} active={} archived={} avg_conf={:.2} low_conf={}",
        fmt_int(global_mem.total),
        fmt_int(global_mem.active),
        fmt_int(global_mem.archived),
        global_mem.avg_confidence,
        fmt_int(global_mem.low_confidence)
    ));
    lines.push(format!(
        "  Global reflector 24h: runs={} +{} ~{} -{}",
        fmt_int(global_mem.reflector_runs_24h),
        fmt_int(global_mem.reflector_inserted_24h),
        fmt_int(global_mem.reflector_updated_24h),
        fmt_int(global_mem.reflector_skipped_24h)
    ));
    lines.push(format!(
        "  Global injection 24h: events={} selected/candidates={}/{}",
        fmt_int(global_mem.injection_events_24h),
        fmt_int(global_mem.injection_selected_24h),
        fmt_int(global_mem.injection_candidates_24h)
    ));
    lines.push("".to_string());
    lines.push("Long-Horizon Learning".to_string());
    lines.push("".to_string());
    lines.push(format!(
        "  This chat: runs={} completed={} failed/cancelled={} with_evidence={} tokens={}/{} tools={}/{} cost=${:.4}",
        fmt_int(chat_learning.total_runs),
        fmt_int(chat_learning.completed_runs),
        fmt_int(chat_learning.failed_runs),
        fmt_int(chat_learning.verified_runs),
        fmt_int(chat_learning.input_tokens),
        fmt_int(chat_learning.output_tokens),
        fmt_int(chat_learning.tool_calls),
        fmt_int(chat_learning.tool_errors),
        chat_learning.estimated_cost_usd
    ));
    lines.push(format!(
        "  Global: runs={} completed={} failed/cancelled={} with_evidence={} tokens={}/{} tools={}/{} cost=${:.4}",
        fmt_int(global_learning.total_runs),
        fmt_int(global_learning.completed_runs),
        fmt_int(global_learning.failed_runs),
        fmt_int(global_learning.verified_runs),
        fmt_int(global_learning.input_tokens),
        fmt_int(global_learning.output_tokens),
        fmt_int(global_learning.tool_calls),
        fmt_int(global_learning.tool_errors),
        global_learning.estimated_cost_usd
    ));
    if skill_learning.is_empty() {
        lines.push("  Skills: no governed versions yet".to_string());
    } else {
        lines.push("  Governed skills:".to_string());
        for skill in skill_learning.iter().take(8) {
            lines.push(format!(
                "    - {} v{} [{}] outcomes={} pass_rate={:.0}% utility_lb={:.0}%",
                skill.skill_name,
                skill.active_version,
                skill.state,
                fmt_int(skill.total_outcomes),
                skill.success_rate * 100.0,
                skill.utility_lower_bound * 100.0
            ));
        }
    }

    Ok(lines.join("\n"))
}