ares-server 0.7.5

A.R.E.S - Agentic Retrieval Enhanced Server: A production-grade agentic chatbot server with multi-provider LLM support, tool calling, RAG, and MCP integration
Documentation
//! Database operations for agent_config_versions table.
//!
//! Stores a snapshot of every agent TOON config on startup and on hot-reload,
//! enabling version history, auditing, and rollback (Sprint 12).
//!
//! Schema (migration 008):
//!   id, agent_id, version, config_json (JSONB), is_active, change_source, created_at

use anyhow::Result;
use sqlx::PgPool;
use tracing::{info, instrument, warn};

use crate::utils::toon_config::ToonAgentConfig;

/// Record a batch of agent configs into `agent_config_versions`.
/// Called on startup (change_source="startup") and on hot-reload (change_source="hot_reload").
///
/// Uses INSERT ... ON CONFLICT (agent_id, version) DO NOTHING so the same version
/// is never duplicated — only genuinely new or changed versions create rows.
#[instrument(skip(pool, agents), fields(count = agents.len()))]
pub async fn record_agent_versions(
    pool: &PgPool,
    agents: &[ToonAgentConfig],
    change_source: &str,
) -> Result<()> {
    let mut recorded = 0usize;

    for agent in agents {
        let config_json = serde_json::to_value(agent)
            .unwrap_or_else(|_| serde_json::json!({"name": agent.name}));

        // For rollback events we need the row to be updated even if the version
        // already exists (so the rollback is durably recorded). For startup /
        // hot-reload we keep DO NOTHING to avoid noisy duplicates.
        let sql = if change_source == "rollback" {
            "INSERT INTO agent_config_versions \
             (agent_id, version, config_json, is_active, change_source) \
             VALUES ($1, $2, $3, true, $4) \
             ON CONFLICT (agent_id, version) DO UPDATE \
             SET change_source = EXCLUDED.change_source, is_active = true"
        } else {
            "INSERT INTO agent_config_versions \
             (agent_id, version, config_json, is_active, change_source) \
             VALUES ($1, $2, $3, true, $4) \
             ON CONFLICT (agent_id, version) DO NOTHING"
        };

        match sqlx::query(sql)
        .bind(&agent.name)
        .bind(&agent.version)
        .bind(&config_json)
        .bind(change_source)
        .execute(pool)
        .await
        {
            Ok(r) if r.rows_affected() > 0 => {
                recorded += 1;
            }
            Ok(_) => {
                // Already recorded — skip silently (same version)
            }
            Err(e) => {
                warn!(
                    agent = %agent.name,
                    version = %agent.version,
                    error = %e,
                    "Failed to record agent version"
                );
            }
        }
    }

    if recorded > 0 {
        info!(
            recorded,
            source = change_source,
            "Agent config versions recorded"
        );
    }

    Ok(())
}

/// Get the version history for a specific agent (most recent first)
pub async fn get_agent_version_history(
    pool: &PgPool,
    agent_id: &str,
    limit: i64,
) -> Result<Vec<AgentVersionRecord>> {
    // Runtime `query_as` (not the `query_as!` macro) — same reason as
    // usage.rs: library crates shipped via crates.io cannot assume a
    // DATABASE_URL env var or `.sqlx` cache at downstream compile time.
    let rows = sqlx::query_as::<_, AgentVersionRecord>(
        r#"SELECT id, agent_id, version, config_json, is_active, change_source, created_at
           FROM agent_config_versions
           WHERE agent_id = $1
           ORDER BY created_at DESC
           LIMIT $2"#,
    )
    .bind(agent_id)
    .bind(limit)
    .fetch_all(pool)
    .await?;

    Ok(rows)
}

/// A row from agent_config_versions
#[derive(Debug, Clone, serde::Serialize, sqlx::FromRow)]
pub struct AgentVersionRecord {
    pub id: String,
    pub agent_id: String,
    pub version: String,
    pub config_json: serde_json::Value,
    pub is_active: bool,
    pub change_source: String,
    pub created_at: chrono::DateTime<chrono::Utc>,
}