//! Template & synchronisation use cases (ARCHITECTURE §6, §8; L7). //! //! Two concerns live here: //! - **Templates** (global IDE store): CRUD + monotonic versioning //! ([`CreateTemplate`], [`UpdateTemplate`], [`ListTemplates`], [`DeleteTemplate`]). //! - **Template → agent link**: instantiating an agent from a template //! ([`CreateAgentFromTemplate`]), detecting when a synchronized agent is behind //! its template ([`DetectAgentDrift`]), and applying the update //! ([`SyncAgentWithTemplate`]). //! //! Every use case talks only to ports ([`TemplateStore`], [`AgentContextStore`], //! [`IdGenerator`], [`EventBus`]). use std::sync::Arc; use domain::ports::{AgentContextStore, EventBus, IdGenerator, StoreError, TemplateStore}; use domain::{ Agent, AgentId, AgentManifest, AgentOrigin, AgentTemplate, DomainEvent, ManifestEntry, MarkdownDoc, ProfileId, Project, TemplateId, TemplateVersion, }; use crate::agent::unique_md_path; use crate::error::AppError; // --------------------------------------------------------------------------- // CreateTemplate // --------------------------------------------------------------------------- /// Input for [`CreateTemplate::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CreateTemplateInput { /// Display name. pub name: String, /// Initial Markdown content. pub content: String, /// Default runtime profile for agents created from this template. pub default_profile_id: ProfileId, } /// Output of [`CreateTemplate::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CreateTemplateOutput { /// The created template (at [`TemplateVersion::INITIAL`]). pub template: AgentTemplate, } /// Creates a template in the global store at the initial version. pub struct CreateTemplate { templates: Arc, ids: Arc, } impl CreateTemplate { /// Builds the use case from its ports. #[must_use] pub fn new(templates: Arc, ids: Arc) -> Self { Self { templates, ids } } /// Executes creation. /// /// # Errors /// - [`AppError::Invalid`] if the name is empty, /// - [`AppError::Store`] on persistence failure. pub async fn execute( &self, input: CreateTemplateInput, ) -> Result { let id = TemplateId::from_uuid(self.ids.new_uuid()); let template = AgentTemplate::new( id, input.name, MarkdownDoc::new(input.content), input.default_profile_id, ) .map_err(|e| AppError::Invalid(e.to_string()))?; self.templates.save(&template).await?; Ok(CreateTemplateOutput { template }) } } // --------------------------------------------------------------------------- // UpdateTemplate // --------------------------------------------------------------------------- /// Input for [`UpdateTemplate::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct UpdateTemplateInput { /// Template to update. pub template_id: TemplateId, /// New Markdown content. pub content: String, } /// Output of [`UpdateTemplate::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct UpdateTemplateOutput { /// The updated template (version bumped by one). pub template: AgentTemplate, } /// Updates a template's content and **bumps its version** (monotonic, §8.1), /// announcing [`DomainEvent::TemplateUpdated`] so drift can be re-evaluated. pub struct UpdateTemplate { templates: Arc, events: Arc, } impl UpdateTemplate { /// Builds the use case from its ports. #[must_use] pub fn new(templates: Arc, events: Arc) -> Self { Self { templates, events } } /// Executes the update. /// /// # Errors /// - [`AppError::NotFound`] if the template is unknown, /// - [`AppError::Store`] on persistence failure. pub async fn execute( &self, input: UpdateTemplateInput, ) -> Result { let current = self.templates.get(input.template_id).await?; let updated = current.with_updated_content(MarkdownDoc::new(input.content)); self.templates.save(&updated).await?; self.events.publish(DomainEvent::TemplateUpdated { template_id: updated.id, version: updated.version, }); Ok(UpdateTemplateOutput { template: updated }) } } // --------------------------------------------------------------------------- // ReadTemplate / ListTemplates / DeleteTemplate // --------------------------------------------------------------------------- /// Input for [`ReadTemplate::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ReadTemplateInput { /// Template to read. pub template_id: TemplateId, } /// Output of [`ReadTemplate::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ReadTemplateOutput { /// The requested template. pub template: AgentTemplate, } /// Reads one template from the global store. pub struct ReadTemplate { templates: Arc, } impl ReadTemplate { /// Builds the use case. #[must_use] pub fn new(templates: Arc) -> Self { Self { templates } } /// Reads one template. /// /// # Errors /// - [`AppError::NotFound`] if the template is unknown, /// - [`AppError::Store`] on persistence failure. pub async fn execute(&self, input: ReadTemplateInput) -> Result { Ok(ReadTemplateOutput { template: self.templates.get(input.template_id).await?, }) } } /// Output of [`ListTemplates::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ListTemplatesOutput { /// All templates in the global store. pub templates: Vec, } /// Lists the templates in the global store. pub struct ListTemplates { templates: Arc, } impl ListTemplates { /// Builds the use case. #[must_use] pub fn new(templates: Arc) -> Self { Self { templates } } /// Lists templates. /// /// # Errors /// [`AppError::Store`] on persistence failure. pub async fn execute(&self) -> Result { Ok(ListTemplatesOutput { templates: self.templates.list().await?, }) } } /// Input for [`DeleteTemplate::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DeleteTemplateInput { /// Template to delete. pub template_id: TemplateId, } /// Deletes a template from the global store. /// /// Agents previously created from it keep their `.md` (their `origin` still /// references the now-absent template; drift detection simply finds nothing to /// compare against). pub struct DeleteTemplate { templates: Arc, } impl DeleteTemplate { /// Builds the use case. #[must_use] pub fn new(templates: Arc) -> Self { Self { templates } } /// Deletes the template. /// /// # Errors /// - [`AppError::NotFound`] if the template is unknown, /// - [`AppError::Store`] on persistence failure. pub async fn execute(&self, input: DeleteTemplateInput) -> Result<(), AppError> { self.templates.delete(input.template_id).await?; Ok(()) } } // --------------------------------------------------------------------------- // CreateAgentFromTemplate // --------------------------------------------------------------------------- /// Input for [`CreateAgentFromTemplate::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CreateAgentFromTemplateInput { /// The project that owns the agent. pub project: Project, /// Source template. pub template_id: TemplateId, /// Optional agent name; defaults to the template's name. pub name: Option, /// Whether the agent tracks the template (`true` ⇒ future syncs apply). pub synchronized: bool, } /// Output of [`CreateAgentFromTemplate::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CreateAgentFromTemplateOutput { /// The created agent. pub agent: Agent, } /// Instantiates a project agent from a template: copies the template's /// `content_md` into the agent's `.md`, links the origin to the template at its /// current version, and records the manifest entry. pub struct CreateAgentFromTemplate { templates: Arc, contexts: Arc, ids: Arc, events: Arc, } impl CreateAgentFromTemplate { /// Builds the use case from its ports. #[must_use] pub fn new( templates: Arc, contexts: Arc, ids: Arc, events: Arc, ) -> Self { Self { templates, contexts, ids, events, } } /// Executes creation from a template. /// /// # Errors /// - [`AppError::NotFound`] if the template is unknown, /// - [`AppError::Invalid`] if the resulting agent/manifest is invalid, /// - [`AppError::Store`] on persistence failure. pub async fn execute( &self, input: CreateAgentFromTemplateInput, ) -> Result { let template = self.templates.get(input.template_id).await?; let manifest = self.contexts.load_manifest(&input.project).await?; let name = input.name.unwrap_or_else(|| template.name.clone()); let id = AgentId::from_uuid(self.ids.new_uuid()); let md_path = unique_md_path(&name, &manifest); let origin = AgentOrigin::FromTemplate { template_id: template.id, synced_template_version: template.version, }; let agent = Agent::new( id, name, md_path, template.default_profile_id, origin, input.synchronized, ) .map_err(|e| AppError::Invalid(e.to_string()))?; let mut entries = manifest.entries; entries.push(ManifestEntry::from_agent(&agent)); let manifest = AgentManifest::new(manifest.version, entries) .map_err(|e| AppError::Invalid(e.to_string()))?; self.contexts .save_manifest(&input.project, &manifest) .await?; // Seed the agent context with the template content. self.contexts .write_context(&input.project, &agent.id, &template.content_md) .await?; self.events.publish(DomainEvent::LayoutChanged { project_id: input.project.id, }); Ok(CreateAgentFromTemplateOutput { agent }) } } // --------------------------------------------------------------------------- // DetectAgentDrift // --------------------------------------------------------------------------- /// One drifting agent: its template moved ahead of the agent's synced version. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AgentDrift { /// The drifting agent. pub agent_id: AgentId, /// Version the agent is currently synced to. pub from: TemplateVersion, /// Version available from the template. pub to: TemplateVersion, } /// Input for [`DetectAgentDrift::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DetectAgentDriftInput { /// The project whose agents to check. pub project: Project, } /// Output of [`DetectAgentDrift::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DetectAgentDriftOutput { /// Agents whose template has a newer version (only `synchronized` ones). pub drifts: Vec, } /// Detects which synchronized agents are behind their template /// (`template.version > synced_template_version`, §8.2), announcing /// [`DomainEvent::AgentDriftDetected`] for each. pub struct DetectAgentDrift { templates: Arc, contexts: Arc, events: Arc, } impl DetectAgentDrift { /// Builds the use case from its ports. #[must_use] pub fn new( templates: Arc, contexts: Arc, events: Arc, ) -> Self { Self { templates, contexts, events, } } /// Computes the drift set. /// /// # Errors /// [`AppError::Store`] on persistence failure (a *deleted* template is not an /// error — that agent simply has nothing to drift against). pub async fn execute( &self, input: DetectAgentDriftInput, ) -> Result { let manifest = self.contexts.load_manifest(&input.project).await?; let mut drifts = Vec::new(); for entry in &manifest.entries { // Only synchronized, template-backed agents can drift. if !entry.synchronized { continue; } let (Some(template_id), Some(synced)) = (entry.template_id, entry.synced_template_version) else { continue; }; let template = match self.templates.get(template_id).await { Ok(t) => t, Err(StoreError::NotFound) => continue, Err(e) => return Err(e.into()), }; if template.version > synced { let drift = AgentDrift { agent_id: entry.agent_id, from: synced, to: template.version, }; self.events.publish(DomainEvent::AgentDriftDetected { agent_id: drift.agent_id, from: drift.from, to: drift.to, }); drifts.push(drift); } } Ok(DetectAgentDriftOutput { drifts }) } } // --------------------------------------------------------------------------- // SyncAgentWithTemplate // --------------------------------------------------------------------------- /// Input for [`SyncAgentWithTemplate::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct SyncAgentWithTemplateInput { /// The owning project. pub project: Project, /// The agent to bring up to date. pub agent_id: AgentId, } /// Output of [`SyncAgentWithTemplate::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct SyncAgentWithTemplateOutput { /// Whether an update was applied (`false` for a non-synchronized or /// scratch agent — those are intentionally left untouched, §8.4). pub synced: bool, /// The version the agent is now at, when a sync happened. pub version: Option, } /// Applies a template update to a synchronized agent: **replaces** the agent's /// `.md` with the template content (the context of a synchronized agent is /// "owned" by the template, §8.3) and records the new synced version. Agents /// that are not `synchronized` (or are `scratch`) are left untouched. pub struct SyncAgentWithTemplate { templates: Arc, contexts: Arc, events: Arc, } impl SyncAgentWithTemplate { /// Builds the use case from its ports. #[must_use] pub fn new( templates: Arc, contexts: Arc, events: Arc, ) -> Self { Self { templates, contexts, events, } } /// Executes the sync for one agent. /// /// # Errors /// - [`AppError::NotFound`] if the agent or its template is unknown, /// - [`AppError::Store`] on persistence failure. pub async fn execute( &self, input: SyncAgentWithTemplateInput, ) -> Result { let mut manifest = self.contexts.load_manifest(&input.project).await?; let idx = manifest .entries .iter() .position(|e| e.agent_id == input.agent_id) .ok_or_else(|| AppError::NotFound(format!("agent {}", input.agent_id)))?; let entry = &manifest.entries[idx]; // Non-synchronized / scratch agents are never auto-updated (§8.4). let Some(template_id) = entry.template_id.filter(|_| entry.synchronized) else { return Ok(SyncAgentWithTemplateOutput { synced: false, version: None, }); }; let template = self.templates.get(template_id).await?; manifest.entries[idx].synced_template_version = Some(template.version); // Persist the manifest (revalidated) and overwrite the agent context. let manifest = AgentManifest::new(manifest.version, manifest.entries) .map_err(|e| AppError::Invalid(e.to_string()))?; self.contexts .save_manifest(&input.project, &manifest) .await?; self.contexts .write_context(&input.project, &input.agent_id, &template.content_md) .await?; self.events.publish(DomainEvent::AgentSynced { agent_id: input.agent_id, to: template.version, }); Ok(SyncAgentWithTemplateOutput { synced: true, version: Some(template.version), }) } }