//! Agent → team-template lineage. Slice 3.5d of the missions //! consolidation. Populated by the team materialization path when a //! new agent is created from a template role; read by: //! - the `clawmates_skills` MCP server to resolve the caller's //! template + role_slot so it can merge template default skills //! with agent overrides //! - level-up (Slice 8.5) to diff learned-vs-seeded knowledge and //! route "promote to template" proposals use serde::{Deserialize, Serialize}; use sqlx::PgPool; use sqlx::Row; use time::OffsetDateTime; use uuid::Uuid; use crate::DbError; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AgentTemplateLink { pub agent_id: Uuid, pub template_id: Uuid, pub template_version: i32, pub role_slot: String, #[serde(with = "time::serde::rfc3339::option")] pub seeded_at: Option, #[serde(with = "time::serde::rfc3339")] pub created_at: OffsetDateTime, } /// Idempotent link insert. If the agent already has a link (rare — /// only via manual overrides) we UPDATE the version/role rather than /// error, since the intended template may have shifted. pub async fn upsert( pool: &PgPool, agent_id: Uuid, template_id: Uuid, template_version: i32, role_slot: &str, ) -> Result<(), DbError> { sqlx::query( "INSERT INTO agent_template_link (agent_id, template_id, template_version, role_slot) VALUES ($1,$2,$3,$4) ON CONFLICT (agent_id) DO UPDATE SET template_id = EXCLUDED.template_id, template_version = EXCLUDED.template_version, role_slot = EXCLUDED.role_slot", ) .bind(agent_id) .bind(template_id) .bind(template_version) .bind(role_slot) .execute(pool) .await?; Ok(()) } pub async fn mark_seeded(pool: &PgPool, agent_id: Uuid) -> Result<(), DbError> { sqlx::query( "UPDATE agent_template_link SET seeded_at = now() WHERE agent_id = $1 AND seeded_at IS NULL", ) .bind(agent_id) .execute(pool) .await?; Ok(()) } pub async fn get(pool: &PgPool, agent_id: Uuid) -> Result, DbError> { let row = sqlx::query( "SELECT agent_id, template_id, template_version, role_slot, seeded_at, created_at FROM agent_template_link WHERE agent_id = $1", ) .bind(agent_id) .fetch_optional(pool) .await?; Ok(row.map(|r| AgentTemplateLink { agent_id: r.get("agent_id"), template_id: r.get("template_id"), template_version: r.get("template_version"), role_slot: r.get("role_slot"), seeded_at: r.get("seeded_at"), created_at: r.get("created_at"), })) } /// All agents provisioned from a given template — used by level-up /// when a template gains a new version and we want to prompt each /// downstream agent for upgrade. pub async fn agents_for_template( pool: &PgPool, template_id: Uuid, ) -> Result, DbError> { let rows = sqlx::query( "SELECT agent_id, template_id, template_version, role_slot, seeded_at, created_at FROM agent_template_link WHERE template_id = $1 ORDER BY created_at", ) .bind(template_id) .fetch_all(pool) .await?; Ok(rows .into_iter() .map(|r| AgentTemplateLink { agent_id: r.get("agent_id"), template_id: r.get("template_id"), template_version: r.get("template_version"), role_slot: r.get("role_slot"), seeded_at: r.get("seeded_at"), created_at: r.get("created_at"), }) .collect()) }